From f10076a4ad599c317eb214c81c764d49ffdd313b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Dec 2019 22:52:20 -0800 Subject: [PATCH 01/28] Bumped version to 0.103.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fab722ea5e6..07a4284c491 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.dev0" +PATCH_VERSION = "0b0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 22e7ece31586ac3ec30e9a8e06de8bd1e7e41d35 Mon Sep 17 00:00:00 2001 From: David K <142583+neffs@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:07:45 +0100 Subject: [PATCH 02/28] Limit available heat/cool modes for HomeKit thermostats (#28586) * Limit available heat/cool modes for HomeKit thermostats. The Home app only shows appropriate modes (heat/cool/auto) for the device. Depending on the climate integration, disabling the auto start might be needed. * Include improved mapping for HVAC modes in tests --- .../components/homekit/type_thermostats.py | 57 ++++++++++-- .../homekit/test_type_thermostats.py | 87 ++++++++++++++----- 2 files changed, 115 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 9adc3cc0600..b6e1e75d3c6 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,6 +7,7 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, + ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_HIGH, @@ -23,6 +24,8 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_FAN_ONLY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -60,13 +63,18 @@ from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) +HC_HOMEKIT_VALID_MODES_WATER_HEATER = { + "Heat": 1, +} UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT = { HVAC_MODE_OFF: 0, HVAC_MODE_HEAT: 1, HVAC_MODE_COOL: 2, + HVAC_MODE_AUTO: 3, HVAC_MODE_HEAT_COOL: 3, + HVAC_MODE_FAN_ONLY: 2, } HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} @@ -97,9 +105,9 @@ class Thermostat(HomeAccessory): # Add additional characteristics if auto mode is supported self.chars = [] - features = self.hass.states.get(self.entity_id).attributes.get( - ATTR_SUPPORTED_FEATURES, 0 - ) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_TARGET_TEMPERATURE_RANGE: self.chars.extend( (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -107,12 +115,44 @@ class Thermostat(HomeAccessory): serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) - # Current and target mode characteristics + # Current mode characteristics self.char_current_heat_cool = serv_thermostat.configure_char( CHAR_CURRENT_HEATING_COOLING, value=0 ) + + # Target mode characteristics + hc_modes = state.attributes.get(ATTR_HVAC_MODES, None) + if hc_modes is None: + _LOGGER.error( + "%s: HVAC modes not yet available. Please disable auto start for homekit.", + self.entity_id, + ) + hc_modes = ( + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + ) + + # determine available modes for this entity, prefer AUTO over HEAT_COOL and COOL over FAN_ONLY + self.hc_homekit_to_hass = { + c: s + for s, c in HC_HASS_TO_HOMEKIT.items() + if ( + s in hc_modes + and not ( + (s == HVAC_MODE_HEAT_COOL and HVAC_MODE_AUTO in hc_modes) + or (s == HVAC_MODE_FAN_ONLY and HVAC_MODE_COOL in hc_modes) + ) + ) + } + hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()} + self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=0, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=0, + setter_callback=self.set_heat_cool, + valid_values=hc_valid_values, ) # Current and target temperature characteristics @@ -185,7 +225,7 @@ class Thermostat(HomeAccessory): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) self._flag_heat_cool = True - hass_value = HC_HOMEKIT_TO_HASS[value] + hass_value = self.hc_homekit_to_hass[value] params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HVAC_MODE: hass_value} self.call_service( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value @@ -318,7 +358,10 @@ class WaterHeater(HomeAccessory): CHAR_CURRENT_HEATING_COOLING, value=1 ) self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=1, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=1, + setter_callback=self.set_heat_cool, + valid_values=HC_HOMEKIT_VALID_MODES_WATER_HEATER, ) self.char_current_temp = serv_thermostat.configure_char( diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index c896ad211e8..9f9ebcdfd32 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -24,6 +24,8 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_AUTO, ) from homeassistant.components.homekit.const import ( ATTR_VALUE, @@ -64,7 +66,20 @@ async def test_thermostat(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" - hass.states.async_set(entity_id, HVAC_MODE_OFF) + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) await hass.async_add_job(acc.run) @@ -120,7 +135,7 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 25.0, @@ -164,9 +179,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -183,7 +197,6 @@ async def test_thermostat(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 25.0, ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, @@ -198,9 +211,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -226,14 +238,23 @@ async def test_thermostat(hass, hk_driver, cls, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "19.0°C" - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 2) await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT - assert acc.char_target_heat_cool.value == 1 + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + assert acc.char_target_heat_cool.value == 2 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == HVAC_MODE_HEAT + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_COOL + + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + assert acc.char_target_heat_cool.value == 3 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_AUTO async def test_thermostat_auto(hass, hk_driver, cls, events): @@ -261,7 +282,6 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, @@ -278,9 +298,8 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, @@ -291,15 +310,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): assert acc.char_heating_thresh_temp.value == 19.0 assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_current_heat_cool.value == 2 - assert acc.char_target_heat_cool.value == 3 + assert acc.char_target_heat_cool.value == 2 assert acc.char_current_temp.value == 24.0 assert acc.char_display_units.value == 0 hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, @@ -346,7 +364,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): HVAC_MODE_HEAT, { ATTR_SUPPORTED_FEATURES: 4096, - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -364,7 +381,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -378,7 +394,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_OFF, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -423,7 +438,6 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 75.2, ATTR_TARGET_TEMP_LOW: 68.1, ATTR_TEMPERATURE: 71.6, @@ -503,6 +517,34 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0 +async def test_thermostat_hvac_modes(hass, hk_driver, cls): + """Test if unsupported HVAC modes are deactivated in HomeKit.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 0 + + await hass.async_add_job(acc.char_target_heat_cool.set_value, 1) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 2) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + async def test_water_heater(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" @@ -571,7 +613,8 @@ async def test_water_heater(hass, hk_driver, cls, events): await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 From a4fc4bb2812b968fc8d0612f63bd6f5613bb8d08 Mon Sep 17 00:00:00 2001 From: tetienne Date: Thu, 5 Dec 2019 21:33:56 +0100 Subject: [PATCH 03/28] Increase somfy SCAN_INTERVAL (#29524) - There was too many errors 504 --- homeassistant/components/somfy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 184e32f1e6d..1368725777b 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -26,7 +26,7 @@ DEVICES = "devices" _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=30) DOMAIN = "somfy" From d9280330a627736d7ec2c88ea3a365f47a59b120 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 6 Dec 2019 16:55:42 +1100 Subject: [PATCH 04/28] Bump georss_generic_client to 0.3 (#29532) * bump version of georss_generic_client library * updated requirements --- homeassistant/components/geo_rss_events/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index c681807ad01..b8949286dea 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -3,7 +3,7 @@ "name": "Geo RSS events", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ - "georss_generic_client==0.2" + "georss_generic_client==0.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c6dad0573ca..1fd51ad5d00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 484dba217a8..6b5bcf96d2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 From 2d6599fdd22c2096fe987a1ec259c1a81ff83af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 6 Dec 2019 22:53:26 +0200 Subject: [PATCH 05/28] Huawei LTE device tracker fixes (#29551) * Include MAC address in device state attributes for absent devices too * Use MAC address as default name whether device is connected or not * Fix initialization of known entities Closes https://github.com/home-assistant/home-assistant/issues/29354 --- .../components/huawei_lte/device_tracker.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 29d7f437b5f..f5f834fa186 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -2,7 +2,7 @@ import logging import re -from typing import Any, Dict, Set +from typing import Any, Dict, List, Optional, Set import attr from stringcase import snakecase @@ -40,13 +40,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Initialize already tracked entities tracked: Set[str] = set() registry = await entity_registry.async_get_registry(hass) + known_entities: List[HuaweiLteScannerEntity] = [] for entity in registry.entities.values(): if ( entity.domain == DEVICE_TRACKER_DOMAIN and entity.config_entry_id == config_entry.entry_id ): tracked.add(entity.unique_id) - async_add_new_entities(hass, router.url, async_add_entities, tracked, True) + known_entities.append( + HuaweiLteScannerEntity(router, entity.unique_id.partition("-")[2]) + ) + async_add_entities(known_entities, True) # Tell parent router to poll hosts list to gather new devices router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) @@ -66,13 +70,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_new_entities(hass, router.url, async_add_entities, tracked) -def async_add_new_entities( - hass, router_url, async_add_entities, tracked, included: bool = False -): - """Add new entities. - - :param included: if True, setup only items in tracked, and vice versa - """ +def async_add_new_entities(hass, router_url, async_add_entities, tracked): + """Add new entities that are not already being tracked.""" router = hass.data[DOMAIN].routers[router_url] try: hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] @@ -83,8 +82,7 @@ def async_add_new_entities( new_entities = [] for host in (x for x in hosts if x.get("MacAddress")): entity = HuaweiLteScannerEntity(router, host["MacAddress"]) - tracking = entity.unique_id in tracked - if tracking != included: + if entity.unique_id in tracked: continue tracked.add(entity.unique_id) new_entities.append(entity) @@ -113,12 +111,16 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): mac: str = attr.ib() _is_connected: bool = attr.ib(init=False, default=False) - _name: str = attr.ib(init=False, default="device") + _hostname: Optional[str] = attr.ib(init=False, default=None) _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict) + def __attrs_post_init__(self): + """Initialize internal state.""" + self._device_state_attributes["mac_address"] = self.mac + @property def _entity_name(self) -> str: - return self._name + return self._hostname or self.mac @property def _device_unique_id(self) -> str: @@ -145,8 +147,7 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) self._is_connected = host is not None if self._is_connected: - # HostName may be present with explicit None value - self._name = host.get("HostName") or self.mac + self._hostname = host.get("HostName") self._device_state_attributes = { _better_snakecase(k): v for k, v in host.items() if k != "HostName" } From 90de5652b9621ab30d1602fdc90e83f49384f20e Mon Sep 17 00:00:00 2001 From: Santobert Date: Mon, 9 Dec 2019 10:46:25 +0100 Subject: [PATCH 06/28] Change source of device_info (#29570) --- homeassistant/components/neato/vacuum.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 9cac0cd24ce..d8a3e4ded45 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -165,7 +165,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): _LOGGER.debug("Running Neato Vacuums update") try: if self._robot_stats is None: - self._robot_stats = self.robot.get_robot_info().json() + self._robot_stats = self.robot.get_general_info().json().get("data") except NeatoRobotException: _LOGGER.warning("Couldn't fetch robot information of %s", self._name) @@ -319,10 +319,9 @@ class NeatoConnectedVacuum(StateVacuumDevice): """Device info for neato robot.""" info = {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}, "name": self._name} if self._robot_stats: - info["manufacturer"] = self._robot_stats["data"]["mfg_name"] - info["model"] = self._robot_stats["data"]["modelName"] - if self._state: - info["sw_version"] = self._state["meta"]["firmware"] + info["manufacturer"] = self._robot_stats["battery"]["vendor"] + info["model"] = self._robot_stats["model"] + info["sw_version"] = self._robot_stats["firmware"] def start(self): """Start cleaning or resume cleaning.""" From f7015703bb53bc24011e2686be67a65e93d9c189 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 11:02:26 +0100 Subject: [PATCH 07/28] Only update disabled_by when enabled default and entity enabled states differ (#29643) --- homeassistant/components/unifi/device_tracker.py | 10 ++++++---- homeassistant/components/unifi/sensor.py | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b92211c4eae..46ac3468835 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -76,6 +76,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" for entity in tracked.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY @@ -228,10 +231,9 @@ class UniFiDeviceTracker(ScannerEntity): @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" - if not self.controller.option_track_devices: - return False - - return True + if self.controller.option_track_devices: + return True + return False async def async_added_to_hass(self): """Subscribe to device events.""" diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e4f9b0df6c9..9145fd8e00f 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -40,6 +40,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" for entity in sensors.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY From 70fe5d323827178dfad38eb4da49855eae2ba5c3 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Mon, 9 Dec 2019 12:56:57 +0300 Subject: [PATCH 08/28] Fix unit_of_measurement for Starline temperature sensors (#29740) --- homeassistant/components/starline/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 2507aba4955..ba0cef0255d 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,5 +1,6 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level from .account import StarlineAccount, StarlineDevice @@ -9,8 +10,8 @@ from .entity import StarlineEntity SENSOR_TYPES = { "battery": ["Battery", None, "V", None], "balance": ["Balance", None, None, "mdi:cash-multiple"], - "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, None, None], - "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, None, None], + "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], + "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "gsm_lvl": ["GSM Signal", None, "%", None], } From 19920e48ba18e1f08bde0ed47c3aaa72f833e186 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 9 Dec 2019 13:33:28 +0100 Subject: [PATCH 09/28] HomeAssistant-pyozw 0.1.7 (#29743) --- homeassistant/components/zwave/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 78362b25462..c781a493b55 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": ["homeassistant-pyozw==0.1.6", "pydispatcher==2.0.5"], + "requirements": ["homeassistant-pyozw==0.1.7", "pydispatcher==2.0.5"], "dependencies": [], "codeowners": ["@home-assistant/z-wave"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1fd51ad5d00..8723ad4b372 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ holidays==0.9.11 home-assistant-frontend==20191204.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.6 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b5bcf96d2d..dc5a12ea9e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ holidays==0.9.11 home-assistant-frontend==20191204.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.6 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From d53c6be50adace585b85627d0d18516e8cf28915 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 9 Dec 2019 15:10:03 +0100 Subject: [PATCH 10/28] Updated frontend to 20191204.1 (#29787) --- 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 03d2d7899b0..75d02baaeeb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191204.0" + "home-assistant-frontend==20191204.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e0a823da44e..90a6ac8c8a7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.30 -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8723ad4b372..7392c26562a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -664,7 +664,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc5a12ea9e8..036de45e342 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,7 +228,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 From 807badea9384bb043cd674d7c2630edb304dc0ac Mon Sep 17 00:00:00 2001 From: "Brett T. Warden" Date: Tue, 10 Dec 2019 00:20:52 -0800 Subject: [PATCH 11/28] Bump Roku to 4.0.0 (#29809) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index f2639b31d15..1b5f07eb87a 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -3,7 +3,7 @@ "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": [ - "roku==3.1" + "roku==4.0.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 7392c26562a..9e2f31443e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1742,7 +1742,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==3.1 +roku==4.0.0 # homeassistant.components.roomba roombapy==1.4.2 From 302d5d66ab42067ba29269b36877520072eb4b95 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Dec 2019 13:05:22 +0100 Subject: [PATCH 12/28] Bumped version to 0.103.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 07a4284c491..35ee19da6b5 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 = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 0cb9d0746aed9d80756e5c68b8e82c424ae69469 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 11 Dec 2019 15:43:54 +0100 Subject: [PATCH 13/28] Add user-agent to fix dwd_weather_warnings setup error (#29596) * Added dummy user-agent to http request to fix setup error * Replace dummy user-agent with the user-agent of the global home assistant session * Adjust comment and rename user-agent constant --- homeassistant/components/dwd_weather_warnings/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 4d7ad04e382..2484ba70074 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -19,6 +19,7 @@ from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS @@ -184,7 +185,10 @@ class DwdWeatherWarningsAPI: "jsonp=loadWarnings", ) - self._rest = RestData("GET", resource, None, None, None, True) + # a User-Agent is necessary for this rest api endpoint (#29496) + headers = {"User-Agent": HA_USER_AGENT} + + self._rest = RestData("GET", resource, None, headers, None, True) self.region_name = region_name self.region_id = None self.region_state = None From 4c9c22bc3af4207df1cb49ff9862e950f23fc07a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 Dec 2019 15:45:21 +0100 Subject: [PATCH 14/28] Add more logging to help future debug situations (#29800) --- .../components/unifi/device_tracker.py | 19 +++++++++++++------ homeassistant/components/unifi/switch.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 46ac3468835..200258306c1 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,5 +1,6 @@ """Track devices using UniFi controllers.""" import logging +from pprint import pformat from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN @@ -156,14 +157,17 @@ class UniFiClientTracker(ScannerEntity): Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. """ - LOGGER.debug( - "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac - ) await self.controller.request_update() if self.is_wired and self.client.mac in self.controller.wireless_clients: self.is_wired = False + LOGGER.debug( + "Updating UniFi tracked client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def is_connected(self): """Return true if the client is connected to the network. @@ -241,11 +245,14 @@ class UniFiDeviceTracker(ScannerEntity): async def async_update(self): """Synchronize state with controller.""" - LOGGER.debug( - "Updating UniFi tracked device %s (%s)", self.entity_id, self.device.mac - ) await self.controller.request_update() + LOGGER.debug( + "Updating UniFi tracked device %s\n%s", + self.entity_id, + pformat(self.device.raw), + ) + @property def is_connected(self): """Return true if the device is connected to the network.""" diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 82aa6f0384d..f08c3e65630 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,5 +1,6 @@ """Support for devices connected to UniFi POE.""" import logging +from pprint import pformat from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.switch import SwitchDevice @@ -193,6 +194,16 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): if not self.client.sw_port: self.client.raw["sw_port"] = state.attributes["port"] + async def async_update(self): + """Log client information after update.""" + await super().async_update() + + LOGGER.debug( + "Updating UniFi POE controlled client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def unique_id(self): """Return a unique identifier for this switch.""" From 4dd1c5118676f8afc631b165d09fc20db046163e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 20:05:18 +0100 Subject: [PATCH 15/28] UniFi - honor detection time when UniFi wire bug happens (#29820) --- .../components/unifi/device_tracker.py | 18 ++++++++++++------ tests/components/unifi/test_device_tracker.py | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 200258306c1..086393b85d2 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -129,6 +129,7 @@ class UniFiClientTracker(ScannerEntity): self.client = client self.controller = controller self.is_wired = self.client.mac not in controller.wireless_clients + self.wired_bug = None @property def entity_registry_enabled_default(self): @@ -174,13 +175,18 @@ class UniFiClientTracker(ScannerEntity): If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. """ - if self.is_wired == self.client.is_wired and ( - ( - dt_util.utcnow() - - dt_util.utc_from_timestamp(float(self.client.last_seen)) + if self.is_wired != self.client.is_wired: + if not self.wired_bug: + self.wired_bug = dt_util.utcnow() + since_last_seen = dt_util.utcnow() - self.wired_bug + + else: + self.wired_bug = None + since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp( + float(self.client.last_seen) ) - < self.controller.option_detection_time - ): + + if since_last_seen < self.controller.option_detection_time: return True return False diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 29b16553757..580365bb8bd 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -2,6 +2,8 @@ from copy import copy from datetime import timedelta +from asynctest import patch + from homeassistant import config_entries from homeassistant.components import unifi from homeassistant.components.unifi.const import ( @@ -18,8 +20,6 @@ import homeassistant.util.dt as dt_util from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration -DEFAULT_DETECTION_TIME = timedelta(seconds=300) - CLIENT_1 = { "essid": "ssid", "hostname": "client_1", @@ -203,7 +203,20 @@ async def test_wireless_client_go_wired_issue(hass): await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "not_home" + assert client_1.state == "home" + + with patch.object( + unifi.device_tracker.dt_util, + "utcnow", + return_value=(dt_util.utcnow() + timedelta(minutes=5)), + ): + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) From 2fb36d3dea735ac1b5adb3ce5e67c6a6e3ddaf51 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 20:04:48 +0100 Subject: [PATCH 16/28] UniFi - Handle disabled switches (#29824) --- homeassistant/components/unifi/switch.py | 33 ++++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f08c3e65630..45f74f8882f 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -74,12 +74,13 @@ def update_items(controller, async_add_entities, switches, switches_off): block_client_id = f"block-{client_id}" if block_client_id in switches: - LOGGER.debug( - "Updating UniFi block switch %s (%s)", - switches[block_client_id].entity_id, - switches[block_client_id].client.mac, - ) - switches[block_client_id].async_schedule_update_ha_state() + if switches[block_client_id].enabled: + LOGGER.debug( + "Updating UniFi block switch %s (%s)", + switches[block_client_id].entity_id, + switches[block_client_id].client.mac, + ) + switches[block_client_id].async_schedule_update_ha_state() continue if client_id not in controller.api.clients_all: @@ -88,7 +89,6 @@ def update_items(controller, async_add_entities, switches, switches_off): client = controller.api.clients_all[client_id] switches[block_client_id] = UniFiBlockClientSwitch(client, controller) new_switches.append(switches[block_client_id]) - LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac) # control POE for client_id in controller.api.clients: @@ -96,12 +96,13 @@ def update_items(controller, async_add_entities, switches, switches_off): poe_client_id = f"poe-{client_id}" if poe_client_id in switches: - LOGGER.debug( - "Updating UniFi POE switch %s (%s)", - switches[poe_client_id].entity_id, - switches[poe_client_id].client.mac, - ) - switches[poe_client_id].async_schedule_update_ha_state() + if switches[poe_client_id].enabled: + LOGGER.debug( + "Updating UniFi POE switch %s (%s)", + switches[poe_client_id].entity_id, + switches[poe_client_id].client.mac, + ) + switches[poe_client_id].async_schedule_update_ha_state() continue client = controller.api.clients[client_id] @@ -139,7 +140,6 @@ def update_items(controller, async_add_entities, switches, switches_off): switches[poe_client_id] = UniFiPOEClientSwitch(client, controller) new_switches.append(switches[poe_client_id]) - LOGGER.debug("New UniFi POE switch %s (%s)", client.hostname, client.mac) if new_switches: async_add_entities(new_switches) @@ -180,6 +180,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi POE switch %s (%s)", self.name, self.client.mac) state = await self.async_get_last_state() if state is None: @@ -263,6 +264,10 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Representation of a blockable client.""" + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi Block switch %s (%s)", self.name, self.client.mac) + @property def unique_id(self): """Return a unique identifier for this switch.""" From ce041f131e84593d2e4f44a09cf6ece5d9be32db Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 11 Dec 2019 13:28:50 +0100 Subject: [PATCH 17/28] Remove uvloop event policy (#29835) * Remove uvloop event policy * Clean tests * Fix lint * Cleanup statment --- homeassistant/__main__.py | 12 +----------- tests/conftest.py | 6 ------ 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d2903370f49..40336c19592 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -15,12 +15,10 @@ if TYPE_CHECKING: def set_loop() -> None: - """Attempt to use uvloop.""" + """Attempt to use different loop.""" import asyncio from asyncio.events import BaseDefaultEventLoopPolicy - policy = None - if sys.platform == "win32": if hasattr(asyncio, "WindowsProactorEventLoopPolicy"): # pylint: disable=no-member @@ -33,15 +31,7 @@ def set_loop() -> None: _loop_factory = asyncio.ProactorEventLoop policy = ProactorPolicy() - else: - try: - import uvloop - except ImportError: - pass - else: - policy = uvloop.EventLoopPolicy() - if policy is not None: asyncio.set_event_loop_policy(policy) diff --git a/tests/conftest.py b/tests/conftest.py index 5e1bbc76fb5..a6683ecad3a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,6 @@ """Set up some common test helper things.""" -import asyncio import functools import logging -import os from unittest.mock import patch import pytest @@ -26,10 +24,6 @@ from tests.test_util.aiohttp import ( mock_aiohttp_client, ) # noqa: E402 module level import not at top of file -if os.environ.get("UVLOOP") == "1": - import uvloop - - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) logging.basicConfig(level=logging.DEBUG) logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) From 87164e21291472a9f5914c2af2823fe6f192084f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Dec 2019 15:55:23 +0100 Subject: [PATCH 18/28] Bumped version to 0.103.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 35ee19da6b5..e312bd4c257 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 = "0b1" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 628a148944afaafd8cdd15075867b362d5ed6982 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Dec 2019 00:28:56 -0800 Subject: [PATCH 19/28] 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 20/28] 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 21/28] 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 22/28] 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 23/28] 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 24/28] 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 25/28] 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 26/28] 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 27/28] 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 28/28] 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)