From b81b57cdf7e88c69d4a10fba5bd7b8837c7b9949 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 15 Jul 2020 21:29:28 +0200 Subject: [PATCH 01/33] Bumped version to 0.113.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cbb236a426c..d0a8f0bf517 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 408b52de1b98e4b1becc3694afc96f8e1175a9e7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 15 Jul 2020 21:35:33 +0200 Subject: [PATCH 02/33] Update frontend to 20200715.1 (#37888) --- 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 7373273db21..6bf6c9992a0 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200715.0"], + "requirements": ["home-assistant-frontend==20200715.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f0d07ad3e6..27fc4ee5d09 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.34.7 -home-assistant-frontend==20200715.0 +home-assistant-frontend==20200715.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 netdisco==2.8.0 diff --git a/requirements_all.txt b/requirements_all.txt index adf22a7d02a..daac1810b79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -724,7 +724,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200715.0 +home-assistant-frontend==20200715.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4020bcc387..459bced30c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200715.0 +home-assistant-frontend==20200715.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 979260f4be5077e58dd49d8b7427470be44754ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 16 Jul 2020 10:08:05 +0200 Subject: [PATCH 03/33] Fix swapped variables deprecation in log message (#37901) --- homeassistant/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2ffa69b4ff2..a327ca630f8 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -511,8 +511,8 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non elif LEGACY_CONF_WHITELIST_EXTERNAL_DIRS in config: _LOGGER.warning( "Key %s has been replaced with %s. Please update your config", - CONF_ALLOWLIST_EXTERNAL_DIRS, LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, + CONF_ALLOWLIST_EXTERNAL_DIRS, ) hac.allowlist_external_dirs.update( set(config[LEGACY_CONF_WHITELIST_EXTERNAL_DIRS]) From 32858bcea31b326a25cb75de2f43a84dece17c72 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 16 Jul 2020 14:03:43 -0500 Subject: [PATCH 04/33] Fix automation & script restart mode bug (#37909) --- homeassistant/helpers/script.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 29d1acf0316..1ca13e22e9f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -760,14 +760,16 @@ class Script: raise async def _async_stop(self, update_state): - await asyncio.wait([run.async_stop() for run in self._runs]) + aws = [run.async_stop() for run in self._runs] + if not aws: + return + await asyncio.wait(aws) if update_state: self._changed() async def async_stop(self, update_state: bool = True) -> None: """Stop running script.""" - if self.is_running: - await asyncio.shield(self._async_stop(update_state)) + await asyncio.shield(self._async_stop(update_state)) async def _async_get_condition(self, config): config_cache_key = frozenset((k, str(v)) for k, v in config.items()) From 06bc98a3a26d6b54432bcfb14227dacb2134da6c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 16 Jul 2020 20:18:31 +0200 Subject: [PATCH 05/33] Updated frontend to 20200716.0 (#37910) --- 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 6bf6c9992a0..ad68adfd490 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200715.1"], + "requirements": ["home-assistant-frontend==20200716.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 27fc4ee5d09..972c80ea6bc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.34.7 -home-assistant-frontend==20200715.1 +home-assistant-frontend==20200716.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 netdisco==2.8.0 diff --git a/requirements_all.txt b/requirements_all.txt index daac1810b79..76468fa970f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -724,7 +724,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200715.1 +home-assistant-frontend==20200716.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 459bced30c1..e2b2c32f484 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200715.1 +home-assistant-frontend==20200716.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c0302e6ecacbaba383dc94458f779db4dee57fea Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 16 Jul 2020 16:25:42 -0400 Subject: [PATCH 06/33] Fix ZHA electrical measurement sensor initialization (#37915) * Refactor cached ZHA channel reads. If doing a cached ZCL attribute read, do "only_from_cache" read for battery operated devices only. Mains operated devices will do a network read in case of a cache miss. * Use cached attributes for ZHA electrical measurement * Bump up ZHA zigpy dependency. --- .../components/zha/core/channels/base.py | 4 +- .../zha/core/channels/homeautomation.py | 28 ++++------ homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_sensor.py | 55 +++++++++++++++++++ 6 files changed, 70 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 83accc5b86c..ebc2cd5cd0f 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -245,7 +245,7 @@ class ZigbeeChannel(LogMixin): self._cluster, [attribute], allow_cache=from_cache, - only_cache=from_cache, + only_cache=from_cache and not self._ch_pool.is_mains_powered, manufacturer=manufacturer, ) return result.get(attribute) @@ -260,7 +260,7 @@ class ZigbeeChannel(LogMixin): result, _ = await self.cluster.read_attributes( attributes, allow_cache=from_cache, - only_cache=from_cache, + only_cache=from_cache and not self._ch_pool.is_mains_powered, manufacturer=manufacturer, ) return result diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index d95180ce780..e18f4ae9c17 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -4,7 +4,7 @@ from typing import Optional import zigpy.zcl.clusters.homeautomation as homeautomation -from .. import registries, typing as zha_typing +from .. import registries from ..const import ( CHANNEL_ELECTRICAL_MEASUREMENT, REPORT_CONFIG_DEFAULT, @@ -51,14 +51,6 @@ class ElectricalMeasurementChannel(ZigbeeChannel): REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType - ) -> None: - """Initialize Metering.""" - super().__init__(cluster, ch_pool) - self._divisor = None - self._multiplier = None - async def async_update(self): """Retrieve latest state.""" self.debug("async_update") @@ -80,7 +72,9 @@ class ElectricalMeasurementChannel(ZigbeeChannel): async def fetch_config(self, from_cache): """Fetch config from device and updates format specifier.""" - results = await self.get_attributes( + + # prime the cache + await self.get_attributes( [ "ac_power_divisor", "power_divisor", @@ -89,22 +83,20 @@ class ElectricalMeasurementChannel(ZigbeeChannel): ], from_cache=from_cache, ) - self._divisor = results.get( - "ac_power_divisor", results.get("power_divisor", self._divisor) - ) - self._multiplier = results.get( - "ac_power_multiplier", results.get("power_multiplier", self._multiplier) - ) @property def divisor(self) -> Optional[int]: """Return active power divisor.""" - return self._divisor or 1 + return self.cluster.get( + "ac_power_divisor", self.cluster.get("power_divisor", 1) + ) @property def multiplier(self) -> Optional[int]: """Return active power divisor.""" - return self._multiplier or 1 + return self.cluster.get( + "ac_power_multiplier", self.cluster.get("power_multiplier", 1) + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register( diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e469cc90cc4..24d9a0a3962 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zha-quirks==0.0.42", "zigpy-cc==0.4.4", "zigpy-deconz==0.9.2", - "zigpy==0.22.1", + "zigpy==0.22.2", "zigpy-xbee==0.12.1", "zigpy-zigate==0.6.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index 76468fa970f..d42d86276ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2269,7 +2269,7 @@ zigpy-xbee==0.12.1 zigpy-zigate==0.6.1 # homeassistant.components.zha -zigpy==0.22.1 +zigpy==0.22.2 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2b2c32f484..a318da8ed17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -999,4 +999,4 @@ zigpy-xbee==0.12.1 zigpy-zigate==0.6.1 # homeassistant.components.zha -zigpy==0.22.1 +zigpy==0.22.2 diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 064b0251e6b..25fecd2d82c 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -265,3 +265,58 @@ async def test_temp_uom( assert state is not None assert round(float(state.state)) == expected assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == uom + + +async def test_electrical_measurement_init( + hass, zigpy_device_mock, zha_device_joined, +): + """Test proper initialization of the electrical measurement cluster.""" + + cluster_id = homeautomation.ElectricalMeasurement.cluster_id + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [cluster_id, general.Basic.cluster_id], + "out_cluster": [], + "device_type": 0x0000, + } + } + ) + cluster = zigpy_device.endpoints[1].in_clusters[cluster_id] + zha_device = await zha_device_joined(zigpy_device) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + + # allow traffic to flow through the gateway and devices + await async_enable_traffic(hass, [zha_device]) + + # test that the sensor now have a state of unknown + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) + assert int(hass.states.get(entity_id).state) == 100 + + channel = zha_device.channels.pools[0].all_channels["1:0x0b04"] + assert channel.divisor == 1 + assert channel.multiplier == 1 + + # update power divisor + await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0403: 5, 10: 1000}) + assert channel.divisor == 5 + assert channel.multiplier == 1 + assert hass.states.get(entity_id).state == "4.0" + + await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0605: 10, 10: 1000}) + assert channel.divisor == 10 + assert channel.multiplier == 1 + assert hass.states.get(entity_id).state == "3.0" + + # update power multiplier + await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0402: 6, 10: 1000}) + assert channel.divisor == 10 + assert channel.multiplier == 6 + assert hass.states.get(entity_id).state == "12.0" + + await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0604: 20, 10: 1000}) + assert channel.divisor == 10 + assert channel.multiplier == 20 + assert hass.states.get(entity_id).state == "60.0" From 98347345d1abd6cbf876241fa09b8a677e64e255 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 17 Jul 2020 00:28:49 +0200 Subject: [PATCH 07/33] Fix unavailable when value is zero (#37918) --- homeassistant/components/netatmo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index b3c2cb79675..6aaa7d08975 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -555,7 +555,7 @@ class NetatmoPublicSensor(Entity): @property def available(self): """Return True if entity is available.""" - return bool(self._state) + return self._state is not None def update(self): """Get the latest data from Netatmo API and updates the states.""" From b684007fbb7f6a9f5530e36811c465595267fa17 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 17 Jul 2020 02:26:29 +0200 Subject: [PATCH 08/33] Upgrade pysonos to 0.0.32 (#37923) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 7ce4af02e45..3a8ba58cc61 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.31"], + "requirements": ["pysonos==0.0.32"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/requirements_all.txt b/requirements_all.txt index d42d86276ce..2d2e22f5983 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1623,7 +1623,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.31 +pysonos==0.0.32 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a318da8ed17..ecf468ca6b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -743,7 +743,7 @@ pysmartthings==0.7.1 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.31 +pysonos==0.0.32 # homeassistant.components.spc pyspcwebgw==0.4.0 From de67135e86eb61602de9b3ad7d282d37324a130c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Jul 2020 14:50:06 -1000 Subject: [PATCH 09/33] Ensure a state change tracker setup from inside a state change listener does not fire immediately (#37924) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/event.py | 37 +++++++----- tests/helpers/test_event.py | 101 +++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index c6eeffb974f..a2dfcff7699 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -135,7 +135,9 @@ track_state_change = threaded_listener_factory(async_track_state_change) @bind_hass def async_track_state_change_event( - hass: HomeAssistant, entity_ids: Iterable[str], action: Callable[[Event], None] + hass: HomeAssistant, + entity_ids: Union[str, Iterable[str]], + action: Callable[[Event], None], ) -> Callable[[], None]: """Track specific state change events indexed by entity_id. @@ -161,7 +163,7 @@ def async_track_state_change_event( if entity_id not in entity_callbacks: return - for action in entity_callbacks[entity_id]: + for action in entity_callbacks[entity_id][:]: try: hass.async_run_job(action, event) except Exception: # pylint: disable=broad-except @@ -173,13 +175,13 @@ def async_track_state_change_event( EVENT_STATE_CHANGED, _async_state_change_dispatcher ) + if isinstance(entity_ids, str): + entity_ids = [entity_ids] + entity_ids = [entity_id.lower() for entity_id in entity_ids] for entity_id in entity_ids: - if entity_id not in entity_callbacks: - entity_callbacks[entity_id] = [] - - entity_callbacks[entity_id].append(action) + entity_callbacks.setdefault(entity_id, []).append(action) @callback def remove_listener() -> None: @@ -247,7 +249,7 @@ def async_track_same_state( hass: HomeAssistant, period: timedelta, action: Callable[..., None], - async_check_same_func: Callable[[str, State, State], bool], + async_check_same_func: Callable[[str, Optional[State], Optional[State]], bool], entity_ids: Union[str, Iterable[str]] = MATCH_ALL, ) -> CALLBACK_TYPE: """Track the state of entities for a period and run an action. @@ -279,10 +281,12 @@ def async_track_same_state( hass.async_run_job(action) @callback - def state_for_cancel_listener( - entity: str, from_state: State, to_state: State - ) -> None: + def state_for_cancel_listener(event: Event) -> None: """Fire on changes and cancel for listener if changed.""" + entity: str = event.data["entity_id"] + from_state: Optional[State] = event.data.get("old_state") + to_state: Optional[State] = event.data.get("new_state") + if not async_check_same_func(entity, from_state, to_state): clear_listener() @@ -290,9 +294,16 @@ def async_track_same_state( hass, state_for_listener, dt_util.utcnow() + period ) - async_remove_state_for_cancel = async_track_state_change( - hass, entity_ids, state_for_cancel_listener - ) + if entity_ids == MATCH_ALL: + async_remove_state_for_cancel = hass.bus.async_listen( + EVENT_STATE_CHANGED, state_for_cancel_listener + ) + else: + async_remove_state_for_cancel = async_track_state_change_event( + hass, + [entity_ids] if isinstance(entity_ids, str) else entity_ids, + state_for_cancel_listener, + ) return clear_listener diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 7724a80e8b4..b0034ebaaa6 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1011,3 +1011,104 @@ async def test_async_call_later(hass): assert p_action is action assert p_point == now + timedelta(seconds=3) assert remove is mock() + + +async def test_track_state_change_event_chain_multple_entity(hass): + """Test that adding a new state tracker inside a tracker does not fire right away.""" + tracker_called = [] + chained_tracker_called = [] + + chained_tracker_unsub = [] + tracker_unsub = [] + + @ha.callback + def chained_single_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + chained_tracker_called.append((old_state, new_state)) + + @ha.callback + def single_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + tracker_called.append((old_state, new_state)) + + chained_tracker_unsub.append( + async_track_state_change_event( + hass, ["light.bowl", "light.top"], chained_single_run_callback + ) + ) + + tracker_unsub.append( + async_track_state_change_event( + hass, ["light.bowl", "light.top"], single_run_callback + ) + ) + + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + + assert len(tracker_called) == 2 + assert len(chained_tracker_called) == 1 + assert len(tracker_unsub) == 1 + assert len(chained_tracker_unsub) == 2 + + hass.states.async_set("light.bowl", "off") + await hass.async_block_till_done() + + assert len(tracker_called) == 3 + assert len(chained_tracker_called) == 3 + assert len(tracker_unsub) == 1 + assert len(chained_tracker_unsub) == 3 + + +async def test_track_state_change_event_chain_single_entity(hass): + """Test that adding a new state tracker inside a tracker does not fire right away.""" + tracker_called = [] + chained_tracker_called = [] + + chained_tracker_unsub = [] + tracker_unsub = [] + + @ha.callback + def chained_single_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + chained_tracker_called.append((old_state, new_state)) + + @ha.callback + def single_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + tracker_called.append((old_state, new_state)) + + chained_tracker_unsub.append( + async_track_state_change_event( + hass, "light.bowl", chained_single_run_callback + ) + ) + + tracker_unsub.append( + async_track_state_change_event(hass, "light.bowl", single_run_callback) + ) + + hass.states.async_set("light.bowl", "on") + await hass.async_block_till_done() + + assert len(tracker_called) == 1 + assert len(chained_tracker_called) == 0 + assert len(tracker_unsub) == 1 + assert len(chained_tracker_unsub) == 1 + + hass.states.async_set("light.bowl", "off") + await hass.async_block_till_done() + + assert len(tracker_called) == 2 + assert len(chained_tracker_called) == 1 + assert len(tracker_unsub) == 1 + assert len(chained_tracker_unsub) == 2 From d297be2698511ac769fcff7348ba14df04b2fdf9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Jul 2020 00:51:53 +0000 Subject: [PATCH 10/33] Bumped version to 0.113.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d0a8f0bf517..cf3e1c4cac5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 8b207df8193f83795960829e50518fd38aefdd83 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 19 Jul 2020 01:34:50 +0200 Subject: [PATCH 11/33] Rfxtrx fixes for beta (#37957) --- homeassistant/components/rfxtrx/__init__.py | 34 +++++++++++++++--- .../components/rfxtrx/binary_sensor.py | 28 +++++++++++++-- homeassistant/components/rfxtrx/cover.py | 7 ++-- homeassistant/components/rfxtrx/light.py | 17 ++------- homeassistant/components/rfxtrx/sensor.py | 35 ++++++++++++++----- homeassistant/components/rfxtrx/switch.py | 7 ++-- 6 files changed, 88 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index e4b565cd5d9..54863f86332 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -24,7 +24,7 @@ from homeassistant.const import ( UV_INDEX, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_EVENT, @@ -92,9 +92,15 @@ def _bytearray_string(data): raise vol.Invalid("Data must be a hex string with multiple of two characters") +def _ensure_device(value): + if value is None: + return DEVICE_DATA_SCHEMA({}) + return DEVICE_DATA_SCHEMA(value) + + SERVICE_SEND_SCHEMA = vol.Schema({ATTR_EVENT: _bytearray_string}) -DEVICE_SCHEMA = vol.Schema( +DEVICE_DATA_SCHEMA = vol.Schema( { vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, @@ -110,7 +116,7 @@ BASE_SCHEMA = vol.Schema( { vol.Optional(CONF_DEBUG, default=False): cv.boolean, vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, + vol.Optional(CONF_DEVICES, default={}): {cv.string: _ensure_device}, } ) @@ -337,7 +343,7 @@ def get_device_id(device, data_bits=None): return (f"{device.packettype:x}", f"{device.subtype:x}", id_string) -class RfxtrxDevice(Entity): +class RfxtrxDevice(RestoreEntity): """Represents a Rfxtrx device. Contains the common logic for Rfxtrx lights and switches. @@ -348,6 +354,7 @@ class RfxtrxDevice(Entity): self.signal_repetitions = signal_repetitions self._name = f"{device.type_string} {device.id_string}" self._device = device + self._event = None self._state = None self._device_id = device_id self._unique_id = "_".join(x for x in self._device_id) @@ -355,6 +362,17 @@ class RfxtrxDevice(Entity): if event: self._apply_event(event) + async def async_added_to_hass(self): + """Restore RFXtrx device state (ON/OFF).""" + if self._event: + return + + old_state = await self.async_get_last_state() + if old_state is not None: + event = old_state.attributes.get(ATTR_EVENT) + if event: + self._apply_event(get_rfx_object(event)) + @property def should_poll(self): """No polling needed for a RFXtrx switch.""" @@ -365,6 +383,13 @@ class RfxtrxDevice(Entity): """Return the name of the device if any.""" return self._name + @property + def device_state_attributes(self): + """Return the device state attributes.""" + if not self._event: + return None + return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} + @property def is_on(self): """Return true if device is on.""" @@ -391,6 +416,7 @@ class RfxtrxDevice(Entity): def _apply_event(self, event): """Apply a received event.""" + self._event = event def _send_command(self, command, brightness=0): rfx_object = self.hass.data[DATA_RFXOBJECT] diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 8ec67d4a902..5f6e32437c4 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import event as evt +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -25,6 +26,7 @@ from . import ( get_rfx_object, ) from .const import ( + ATTR_EVENT, COMMAND_OFF_LIST, COMMAND_ON_LIST, DATA_RFXTRX_CONFIG, @@ -54,7 +56,7 @@ async def async_setup_entry( _LOGGER.error("Invalid device: %s", packet_id) continue if not supported(event): - return + continue device_id = get_device_id(event.device, data_bits=entity.get(CONF_DATA_BITS)) if device_id in device_ids: @@ -105,7 +107,7 @@ async def async_setup_entry( ) -class RfxtrxBinarySensor(BinarySensorEntity): +class RfxtrxBinarySensor(BinarySensorEntity, RestoreEntity): """A representation of a RFXtrx binary sensor.""" def __init__( @@ -120,7 +122,7 @@ class RfxtrxBinarySensor(BinarySensorEntity): event=None, ): """Initialize the RFXtrx sensor.""" - self.event = None + self._event = None self._device = device self._name = f"{device.type_string} {device.id_string}" self._device_class = device_class @@ -141,6 +143,13 @@ class RfxtrxBinarySensor(BinarySensorEntity): """Restore RFXtrx switch device state (ON/OFF).""" await super().async_added_to_hass() + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + event = old_state.attributes.get(ATTR_EVENT) + if event: + self._apply_event(get_rfx_object(event)) + self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_EVENT, self._handle_event @@ -152,6 +161,13 @@ class RfxtrxBinarySensor(BinarySensorEntity): """Return the device name.""" return self._name + @property + def device_state_attributes(self): + """Return the device state attributes.""" + if not self._event: + return None + return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} + @property def data_bits(self): """Return the number of data bits.""" @@ -172,6 +188,11 @@ class RfxtrxBinarySensor(BinarySensorEntity): """No polling needed.""" return False + @property + def force_update(self) -> bool: + """We should force updates. Repeated states have meaning.""" + return True + @property def device_class(self): """Return the sensor class.""" @@ -221,6 +242,7 @@ class RfxtrxBinarySensor(BinarySensorEntity): def _apply_event(self, event): """Apply command from rfxtrx.""" + self._event = event if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: self._apply_event_lighting4(event) else: diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 829ff9c8110..a3cefb42cb7 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.cover import CoverEntity -from homeassistant.const import CONF_DEVICES, STATE_OPEN +from homeassistant.const import CONF_DEVICES from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity @@ -86,10 +86,6 @@ class RfxtrxCover(RfxtrxDevice, CoverEntity, RestoreEntity): """Restore RFXtrx cover device state (OPEN/CLOSE).""" await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if old_state is not None: - self._state = old_state.state == STATE_OPEN - self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_EVENT, self._handle_event @@ -120,6 +116,7 @@ class RfxtrxCover(RfxtrxDevice, CoverEntity, RestoreEntity): def _apply_event(self, event): """Apply command from rfxtrx.""" + super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: self._state = True elif event.values["Command"] in COMMAND_OFF_LIST: diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index c248d8b8307..649be7be3fe 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -8,9 +8,8 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, LightEntity, ) -from homeassistant.const import CONF_DEVICES, STATE_ON +from homeassistant.const import CONF_DEVICES from homeassistant.core import callback -from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -93,7 +92,7 @@ async def async_setup_entry( hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, light_update) -class RfxtrxLight(RfxtrxDevice, LightEntity, RestoreEntity): +class RfxtrxLight(RfxtrxDevice, LightEntity): """Representation of a RFXtrx light.""" _brightness = 0 @@ -102,17 +101,6 @@ class RfxtrxLight(RfxtrxDevice, LightEntity, RestoreEntity): """Restore RFXtrx device state (ON/OFF).""" await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if old_state is not None: - self._state = old_state.state == STATE_ON - - # Restore the brightness of dimmable devices - if ( - old_state is not None - and old_state.attributes.get(ATTR_BRIGHTNESS) is not None - ): - self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) - self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_EVENT, self._handle_event @@ -147,6 +135,7 @@ class RfxtrxLight(RfxtrxDevice, LightEntity, RestoreEntity): def _apply_event(self, event): """Apply command from rfxtrx.""" + super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: self._state = True elif event.values["Command"] in COMMAND_OFF_LIST: diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 7c540672c9a..de341307551 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import CONF_DEVICES from homeassistant.core import callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -21,7 +21,7 @@ from . import ( get_device_id, get_rfx_object, ) -from .const import DATA_RFXTRX_CONFIG +from .const import ATTR_EVENT, DATA_RFXTRX_CONFIG _LOGGER = logging.getLogger(__name__) @@ -113,12 +113,12 @@ async def async_setup_entry( hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, sensor_update) -class RfxtrxSensor(Entity): +class RfxtrxSensor(RestoreEntity): """Representation of a RFXtrx sensor.""" def __init__(self, device, device_id, data_type, event=None): """Initialize the sensor.""" - self.event = None + self._event = None self._device = device self._name = f"{device.type_string} {device.id_string} {data_type}" self.data_type = data_type @@ -136,6 +136,13 @@ class RfxtrxSensor(Entity): """Restore RFXtrx switch device state (ON/OFF).""" await super().async_added_to_hass() + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + event = old_state.attributes.get(ATTR_EVENT) + if event: + self._apply_event(get_rfx_object(event)) + self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_EVENT, self._handle_event @@ -149,9 +156,9 @@ class RfxtrxSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if not self.event: + if not self._event: return None - value = self.event.values.get(self.data_type) + value = self._event.values.get(self.data_type) return self._convert_fun(value) @property @@ -162,15 +169,25 @@ class RfxtrxSensor(Entity): @property def device_state_attributes(self): """Return the device state attributes.""" - if not self.event: + if not self._event: return None - return self.event.values + return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def force_update(self) -> bool: + """We should force updates. Repeated states have meaning.""" + return True + @property def device_class(self): """Return a device class for sensor.""" @@ -192,7 +209,7 @@ class RfxtrxSensor(Entity): def _apply_event(self, event): """Apply command from rfxtrx.""" - self.event = event + self._event = event @callback def _handle_event(self, event, device_id): diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index c890e162b2c..7b2a23c1624 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -4,7 +4,7 @@ import logging import RFXtrx as rfxtrxmod from homeassistant.components.switch import SwitchEntity -from homeassistant.const import CONF_DEVICES, STATE_ON +from homeassistant.const import CONF_DEVICES from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity @@ -96,10 +96,6 @@ class RfxtrxSwitch(RfxtrxDevice, SwitchEntity, RestoreEntity): """Restore RFXtrx switch device state (ON/OFF).""" await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if old_state is not None: - self._state = old_state.state == STATE_ON - self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_EVENT, self._handle_event @@ -108,6 +104,7 @@ class RfxtrxSwitch(RfxtrxDevice, SwitchEntity, RestoreEntity): def _apply_event(self, event): """Apply command from rfxtrx.""" + super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: self._state = True elif event.values["Command"] in COMMAND_OFF_LIST: From 7c0a933452e51d3afac22829c8a06430ec7ae6a7 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 17 Jul 2020 14:25:16 +0200 Subject: [PATCH 12/33] Add ozw support for single setpoint thermostat devices (#37713) Co-authored-by: Paulus Schoutsen --- homeassistant/components/ozw/climate.py | 47 +++++++++++++-------- homeassistant/components/ozw/discovery.py | 31 ++++++++++++++ tests/components/ozw/test_climate.py | 45 ++++++++++++++++++++ tests/fixtures/ozw/climate_network_dump.csv | 36 ++++++++++++++++ 4 files changed, 142 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index 8a524805b57..1486d98de2c 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -174,7 +174,8 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): def hvac_mode(self): """Return hvac operation ie. heat, cool mode.""" if not self.values.mode: - return None + # Thermostat(valve) with no support for setting a mode is considered heating-only + return HVAC_MODE_HEAT return ZW_HVAC_MODE_MAPPINGS.get( self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_HEAT_COOL ) @@ -197,7 +198,7 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - if self.values.temperature and self.values.temperature.units == "F": + if self.values.temperature is not None and self.values.temperature.units == "F": return TEMP_FAHRENHEIT return TEMP_CELSIUS @@ -220,6 +221,8 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): def preset_mode(self): """Return preset operation ie. eco, away.""" # A Zwave mode that can not be translated to a hass mode is considered a preset + if not self.values.mode: + return None if self.values.mode.value[VALUE_SELECTED_ID] not in MODES_LIST: return self.values.mode.value[VALUE_SELECTED_LABEL] return PRESET_NONE @@ -274,8 +277,14 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" + if not self.values.mode: + # Thermostat(valve) with no support for setting a mode + _LOGGER.warning( + "Thermostat %s does not support setting a mode", self.entity_id + ) + return hvac_mode_value = self._hvac_modes.get(hvac_mode) - if not hvac_mode_value: + if hvac_mode_value is None: _LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode) return self.values.mode.send_value(hvac_mode_value) @@ -320,8 +329,11 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): def _get_current_mode_setpoint_values(self) -> Tuple: """Return a tuple of current setpoint Z-Wave value(s).""" - current_mode = self.values.mode.value[VALUE_SELECTED_ID] - setpoint_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) + if not self.values.mode: + setpoint_names = ("setpoint_heating",) + else: + current_mode = self.values.mode.value[VALUE_SELECTED_ID] + setpoint_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) # we do not want None values in our tuple so check if the value exists return tuple( getattr(self.values, value_name) @@ -331,20 +343,21 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): def _set_modes_and_presets(self): """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" - if not self.values.mode: - return all_modes = {} all_presets = {PRESET_NONE: None} - # Z-Wave uses one list for both modes and presets. - # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. - for val in self.values.mode.value[VALUE_LIST]: - if val[VALUE_ID] in MODES_LIST: - # treat value as hvac mode - hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) - all_modes[hass_mode] = val[VALUE_ID] - else: - # treat value as hvac preset - all_presets[val[VALUE_LABEL]] = val[VALUE_ID] + if self.values.mode: + # Z-Wave uses one list for both modes and presets. + # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. + for val in self.values.mode.value[VALUE_LIST]: + if val[VALUE_ID] in MODES_LIST: + # treat value as hvac mode + hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) + all_modes[hass_mode] = val[VALUE_ID] + else: + # treat value as hvac preset + all_presets[val[VALUE_LABEL]] = val[VALUE_ID] + else: + all_modes[HVAC_MODE_HEAT] = None self._hvac_modes = all_modes self._hvac_presets = all_presets diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py index 2eaaa3a2714..12690b343fc 100644 --- a/homeassistant/components/ozw/discovery.py +++ b/homeassistant/components/ozw/discovery.py @@ -131,6 +131,37 @@ DISCOVERY_SCHEMAS = ( }, }, }, + { # Z-Wave Thermostat device without mode support + const.DISC_COMPONENT: "climate", + const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_THERMOSTAT,), + const.DISC_SPECIFIC_DEVICE_CLASS: ( + const_ozw.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ), + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,) + }, + "temperature": { + const.DISC_COMMAND_CLASS: (CommandClass.SENSOR_MULTILEVEL,), + const.DISC_INDEX: (1,), + const.DISC_OPTIONAL: True, + }, + "operating_state": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_OPERATING_STATE,), + const.DISC_OPTIONAL: True, + }, + "valve_position": { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), + const.DISC_INDEX: (0,), + const.DISC_OPTIONAL: True, + }, + "setpoint_heating": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (1,), + const.DISC_OPTIONAL: True, + }, + }, + }, { # Rollershutter const.DISC_COMPONENT: "cover", const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,), diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py index 13691b49f65..70fba99f7f2 100644 --- a/tests/components/ozw/test_climate.py +++ b/tests/components/ozw/test_climate.py @@ -6,6 +6,7 @@ from homeassistant.components.climate.const import ( ATTR_FAN_MODES, ATTR_HVAC_ACTION, ATTR_HVAC_MODES, + ATTR_PRESET_MODE, ATTR_PRESET_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -218,3 +219,47 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): ) assert len(sent_messages) == 8 assert "Received an invalid preset mode: invalid preset mode" in caplog.text + + # test thermostat device without a mode commandclass + state = hass.states.get("climate.danfoss_living_connect_z_v1_06_014g0013_heating_1") + assert state is not None + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_HEAT, + ] + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) is None + assert round(state.attributes[ATTR_TEMPERATURE], 0) == 21 + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None + assert state.attributes.get(ATTR_PRESET_MODE) is None + assert state.attributes.get(ATTR_PRESET_MODES) is None + + # Test set target temperature + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.danfoss_living_connect_z_v1_06_014g0013_heating_1", + "temperature": 28.0, + }, + blocking=True, + ) + assert len(sent_messages) == 9 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": 28.0, + "ValueIDKey": 281475116220434, + } + + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.danfoss_living_connect_z_v1_06_014g0013_heating_1", + "hvac_mode": HVAC_MODE_HEAT, + }, + blocking=True, + ) + assert len(sent_messages) == 9 + assert "does not support setting a mode" in caplog.text diff --git a/tests/fixtures/ozw/climate_network_dump.csv b/tests/fixtures/ozw/climate_network_dump.csv index c865e6438de..370edc15be1 100644 --- a/tests/fixtures/ozw/climate_network_dump.csv +++ b/tests/fixtures/ozw/climate_network_dump.csv @@ -72,6 +72,42 @@ OpenZWave/1/node/7/instance/2/commandclass/49/value/72057594168754212/,{ "Lab OpenZWave/1/node/7/instance/2/commandclass/49/value/1407375005990946/,{ "Label": "Instance 2: Humidity", "Value": 56.0, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 5, "Node": 7, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1407375005990946, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264907} OpenZWave/1/node/7/instance/2/commandclass/49/value/73183494075596836/,{ "Label": "Instance 2: Humidity Units", "Value": { "List": [ { "Value": 0, "Label": "Percent" } ], "Selected": "Percent", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 260, "Node": 7, "Genre": "System", "Help": "Humidity Sensor Available Units", "ValueIDKey": 73183494075596836, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} OpenZWave/1/node/7/association/1/,{ "Name": "Reporting", "Help": "", "MaxAssociations": 2, "Members": [ "1.0" ], "TimeStamp": 1588264906} +OpenZWave/1/node/8/,{ "NodeID": 8, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0002:0003:0005", "ZWAProductURL": "https://products.z-wavealliance.org/products/1507/", "ProductPic": "images/danfoss/z.png", "Description": "Electronic radiator thermostat", "ProductManualURL": "", "ProductPageURL": "http://heating.consumers.danfoss.com/xxTypex/585379.html", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "CEPT (Europe)", "Name": "Danfoss Living Connect Z v1.06 014G0013", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1594159718, "NodeManufacturerName": "Danfoss", "NodeProductName": "Z Thermostat 014G0013", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "Setpoint Thermostat", "NodeSpecific": 4, "NodeManufacturerID": "0x0002", "NodeProductType": "0x0005", "NodeProductID": "0x0004", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1 ], "Neighbors": [ 1 ]} +OpenZWave/1/node/8/instance/1/commandclass/70/value/2251799953244180/,{ "Label": "Override State", "Value": { "List": [ { "Value": 0, "Label": "None" }, { "Value": 1, "Label": "Temporary" }, { "Value": 2, "Label": "Permanent" } ], "Selected": "None", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 8, "Node": 8, "Genre": "User", "Help": "Override Schedule", "ValueIDKey": 2251799953244180, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} +OpenZWave/1/node/8/instance/1/commandclass/70/value/2533274929954833/,{ "Label": "Override Setback", "Value": 127, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 9, "Node": 8, "Genre": "User", "Help": "Override Setback", "ValueIDKey": 2533274929954833, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} +OpenZWave/1/node/8/instance/1/commandclass/70/value/281475116269589/,{ "Label": "Monday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 1, "Node": 8, "Genre": "User", "Help": "Schedule for Monday", "ValueIDKey": 281475116269589, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/70/value/562950092980245/,{ "Label": "Tuesday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 2, "Node": 8, "Genre": "User", "Help": "Schedule for Tuesday", "ValueIDKey": 562950092980245, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/70/value/844425069690901/,{ "Label": "Wednesday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 3, "Node": 8, "Genre": "User", "Help": "Schedule for Wednesday", "ValueIDKey": 844425069690901, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/70/value/1125900046401557/,{ "Label": "Thursday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 4, "Node": 8, "Genre": "User", "Help": "Schedule for Thursday", "ValueIDKey": 1125900046401557, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/70/value/1407375023112213/,{ "Label": "Friday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 5, "Node": 8, "Genre": "User", "Help": "Schedule for Friday", "ValueIDKey": 1407375023112213, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/70/value/1688849999822869/,{ "Label": "Saturday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 6, "Node": 8, "Genre": "User", "Help": "Schedule for Saturday", "ValueIDKey": 1688849999822869, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/70/value/1970324976533525/,{ "Label": "Sunday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 7, "Node": 8, "Genre": "User", "Help": "Schedule for Sunday", "ValueIDKey": 1970324976533525, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/70/,{ "Instance": 1, "CommandClassId": 70, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "CommandClassVersion": 1, "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/117/value/148717588/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Unprotected", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 8, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 148717588, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} +OpenZWave/1/node/8/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "CommandClassVersion": 1, "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/128/value/140509201/,{ "Label": "Battery Level", "Value": 79, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 8, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 140509201, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} +OpenZWave/1/node/8/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "CommandClassVersion": 1, "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/129/value/140525588/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Wednesday", "Selected_id": 3 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 8, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 140525588, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} +OpenZWave/1/node/8/instance/1/commandclass/129/value/281475117236241/,{ "Label": "Hour", "Value": 13, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 8, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475117236241, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} +OpenZWave/1/node/8/instance/1/commandclass/129/value/562950093946897/,{ "Label": "Minute", "Value": 17, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 8, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950093946897, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} +OpenZWave/1/node/8/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "CommandClassVersion": 1, "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/67/value/281475116220434/,{ "Label": "Heating 1", "Value": 21.0, "Units": "C", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 8, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475116220434, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} +OpenZWave/1/node/8/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "CommandClassVersion": 2, "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 1, "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/114/value/148668435/,{ "Label": "Loaded Config Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 8, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 148668435, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/114/value/281475125379091/,{ "Label": "Config File Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 8, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475125379091, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/114/value/562950102089747/,{ "Label": "Latest Available Config File Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 8, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950102089747, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "CommandClassVersion": 2, "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/132/value/148963347/,{ "Label": "Wake-up Interval", "Value": 300, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 8, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 148963347, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/132/value/281475125674003/,{ "Label": "Minimum Wake-up Interval", "Value": 60, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 8, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475125674003, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/132/value/562950102384659/,{ "Label": "Maximum Wake-up Interval", "Value": 1800, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 8, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950102384659, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/132/value/844425079095315/,{ "Label": "Default Wake-up Interval", "Value": 300, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 8, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425079095315, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/132/value/1125900055805971/,{ "Label": "Wake-up Interval Step", "Value": 60, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 8, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900055805971, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/134/value/148996119/,{ "Label": "Library Version", "Value": "6", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 8, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 148996119, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/134/value/281475125706775/,{ "Label": "Protocol Version", "Value": "3.67", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 8, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475125706775, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/commandclass/134/value/562950102417431/,{ "Label": "Application Version", "Value": "1.01", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 8, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950102417431, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} +OpenZWave/1/node/8/instance/1/,{ "Instance": 1, "TimeStamp": 1594159418} OpenZWave/1/node/16/,{ "NodeID": 16, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0148:0001:0003", "ZWAProductURL": "https://products.z-wavealliance.org/products/2543/", "ProductPic": "images/eurotronic/eur_spiritz.png", "Description": "• Easy control for water radiators from any Z-Wave Controller • Fits most European water radiators (wide range of additional adaptors for different manufacturers available) • FLiRS for quick response time • LED Backlit LCD • Metal nut for reliable connection to the radiator • 2 buttons for easy temperature regulation • Battery level indicator • Child Lock • Over the Air update • UK-Mode for upside down installation • Open Window detection • Automatic frost protection", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2650/Spirit_Z-Wave_BAL_web_EN_view_05.pdf", "ProductPageURL": "", "InclusionHelp": "Start Inclusion mode of your primary Z-Wave Controller. Press the Boost-Button.", "ExclusionHelp": "Start Exclusion mode of your primary Z-Wave Controller. Now press and hold the boost button of the Spirit Z-Wave Plus for at least 5 seconds.", "ResetHelp": "Please use this procedure only when the network primary controller is missing or otherwise inoperable. Remove batteries. Press and hold boost button. While still holding boost button insert batteries. The LCD shows RES. Release boost button. To perform the factory reset press boost button.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "UAE", "Name": "KOMFORTHAUS Spirit Z-Wave Plus", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1588422766, "NodeManufacturerName": "EUROtronic", "NodeProductName": "EUR_SPIRITZ Wall Radiator Thermostat", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "General Thermostat V2", "NodeSpecific": 6, "NodeManufacturerID": "0x0148", "NodeProductType": "0x0003", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Thermostat HVAC", "NodeDeviceType": 4608, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 7, 8, 9, 10, 12, 13, 14 ]} OpenZWave/1/node/16/instance/1/,{ "Instance": 1, "TimeStamp": 1588422682} OpenZWave/1/node/16/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1588422682} From b784cc011da1e5ed705de0585759bb7263ebf0de Mon Sep 17 00:00:00 2001 From: rajlaud <50647620+rajlaud@users.noreply.github.com> Date: Fri, 17 Jul 2020 11:21:42 -0500 Subject: [PATCH 13/33] Fix bugs updating state of `hdmi_cec` switch (#37786) --- homeassistant/components/hdmi_cec/__init__.py | 11 ++++++++++- homeassistant/components/hdmi_cec/media_player.py | 8 ++++---- homeassistant/components/hdmi_cec/switch.py | 13 ++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 471a2dd0f46..c9a5d27a3be 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -353,7 +353,7 @@ def setup(hass: HomeAssistant, base_config): return True -class CecDevice(Entity): +class CecEntity(Entity): """Representation of a HDMI CEC device entity.""" def __init__(self, device, logical) -> None: @@ -388,6 +388,15 @@ class CecDevice(Entity): """Device status changed, schedule an update.""" self.schedule_update_ha_state(True) + @property + def should_poll(self): + """ + Return false. + + CecEntity.update() is called by the HDMI network when there is new data. + """ + return False + @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 180580ef371..c3cab6a8f98 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -43,7 +43,7 @@ from homeassistant.const import ( STATE_PLAYING, ) -from . import ATTR_NEW, CecDevice +from . import ATTR_NEW, CecEntity _LOGGER = logging.getLogger(__name__) @@ -57,16 +57,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for device in discovery_info[ATTR_NEW]: hdmi_device = hass.data.get(device) - entities.append(CecPlayerDevice(hdmi_device, hdmi_device.logical_address)) + entities.append(CecPlayerEntity(hdmi_device, hdmi_device.logical_address)) add_entities(entities, True) -class CecPlayerDevice(CecDevice, MediaPlayerEntity): +class CecPlayerEntity(CecEntity, MediaPlayerEntity): """Representation of a HDMI device as a Media player.""" def __init__(self, device, logical) -> None: """Initialize the HDMI device.""" - CecDevice.__init__(self, device, logical) + CecEntity.__init__(self, device, logical) self.entity_id = f"{DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" def send_keypress(self, key): diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index aaaa2b83054..ea0cac76a99 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY -from . import ATTR_NEW, CecDevice +from . import ATTR_NEW, CecEntity _LOGGER = logging.getLogger(__name__) @@ -18,27 +18,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for device in discovery_info[ATTR_NEW]: hdmi_device = hass.data.get(device) - entities.append(CecSwitchDevice(hdmi_device, hdmi_device.logical_address)) + entities.append(CecSwitchEntity(hdmi_device, hdmi_device.logical_address)) add_entities(entities, True) -class CecSwitchDevice(CecDevice, SwitchEntity): +class CecSwitchEntity(CecEntity, SwitchEntity): """Representation of a HDMI device as a Switch.""" def __init__(self, device, logical) -> None: """Initialize the HDMI device.""" - CecDevice.__init__(self, device, logical) + CecEntity.__init__(self, device, logical) self.entity_id = f"{DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" def turn_on(self, **kwargs) -> None: """Turn device on.""" self._device.turn_on() self._state = STATE_ON + self.schedule_update_ha_state(force_refresh=False) def turn_off(self, **kwargs) -> None: """Turn device off.""" self._device.turn_off() - self._state = STATE_ON + self._state = STATE_OFF + self.schedule_update_ha_state(force_refresh=False) def toggle(self, **kwargs): """Toggle the entity.""" @@ -47,6 +49,7 @@ class CecSwitchDevice(CecDevice, SwitchEntity): self._state = STATE_OFF else: self._state = STATE_ON + self.schedule_update_ha_state(force_refresh=False) @property def is_on(self) -> bool: From af36a67b89d104c80a779a8b124f7a9d1e7ee9dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Jul 2020 01:25:07 -1000 Subject: [PATCH 14/33] fix (#37889) --- .../components/homekit/type_thermostats.py | 81 +++++++++---------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 10e4e956328..1d0d760b963 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -74,6 +74,13 @@ from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) +DEFAULT_HVAC_MODES = [ + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, +] + HC_HOMEKIT_VALID_MODES_WATER_HEATER = {"Heat": 1} UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} @@ -117,7 +124,6 @@ class Thermostat(HomeAccessory): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = self.hass.config.units.temperature_unit - self._state_updates = 0 self.hc_homekit_to_hass = None self.hc_hass_to_homekit = None hc_min_temp, hc_max_temp = self.get_temperature_range() @@ -237,14 +243,20 @@ class Thermostat(HomeAccessory): # Homekit will reset the mode when VIEWING the temp # Ignore it if its the same mode if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode: - service = SERVICE_SET_HVAC_MODE_THERMOSTAT - hass_value = self.hc_homekit_to_hass[ - char_values[CHAR_TARGET_HEATING_COOLING] - ] - params = {ATTR_HVAC_MODE: hass_value} - events.append( - f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" - ) + target_hc = char_values[CHAR_TARGET_HEATING_COOLING] + if target_hc in self.hc_homekit_to_hass: + service = SERVICE_SET_HVAC_MODE_THERMOSTAT + hass_value = self.hc_homekit_to_hass[target_hc] + params = {ATTR_HVAC_MODE: hass_value} + events.append( + f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" + ) + else: + _LOGGER.warning( + "The entity: %s does not have a %s mode", + self.entity_id, + target_hc, + ) if CHAR_TARGET_TEMPERATURE in char_values: hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE] @@ -321,20 +333,8 @@ class Thermostat(HomeAccessory): def _configure_hvac_modes(self, state): """Configure target mode characteristics.""" - hc_modes = state.attributes.get(ATTR_HVAC_MODES) - if not hc_modes: - # This cannot be none OR an empty list - _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, - ) - + # This cannot be none OR an empty list + hc_modes = state.attributes.get(ATTR_HVAC_MODES) or DEFAULT_HVAC_MODES # Determine available modes for this entity, # Prefer HEAT_COOL over AUTO and COOL over FAN_ONLY, DRY # @@ -379,26 +379,23 @@ class Thermostat(HomeAccessory): @callback def async_update_state(self, new_state): """Update thermostat state after state changed.""" - if self._state_updates < 3: - # When we get the first state updates - # we recheck valid hvac modes as the entity - # may not have been fully setup when we saw it the - # first time - original_hc_hass_to_homekit = self.hc_hass_to_homekit - self._configure_hvac_modes(new_state) - if self.hc_hass_to_homekit != original_hc_hass_to_homekit: - if self.char_target_heat_cool.value not in self.hc_homekit_to_hass: - # We must make sure the char value is - # in the new valid values before - # setting the new valid values or - # changing them with throw - self.char_target_heat_cool.set_value( - list(self.hc_homekit_to_hass)[0], should_notify=False - ) - self.char_target_heat_cool.override_properties( - valid_values=self.hc_hass_to_homekit + # We always recheck valid hvac modes as the entity + # may not have been fully setup when we saw it last + original_hc_hass_to_homekit = self.hc_hass_to_homekit + self._configure_hvac_modes(new_state) + + if self.hc_hass_to_homekit != original_hc_hass_to_homekit: + if self.char_target_heat_cool.value not in self.hc_homekit_to_hass: + # We must make sure the char value is + # in the new valid values before + # setting the new valid values or + # changing them with throw + self.char_target_heat_cool.set_value( + list(self.hc_homekit_to_hass)[0], should_notify=False ) - self._state_updates += 1 + self.char_target_heat_cool.override_properties( + valid_values=self.hc_hass_to_homekit + ) self._async_update_state(new_state) From 253950f84f819d22f2b93aacec1c042a7477cc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 17 Jul 2020 15:04:04 +0100 Subject: [PATCH 15/33] Change ZHA power unit from kW to W (#37896) * Change ZHA power unit from kW to W * Use POWER_WATT * Move kW to W conversion; ignore unit for power --- .../components/zha/core/channels/smartenergy.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 7b12411b84f..9138ea09782 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -3,7 +3,7 @@ import logging import zigpy.zcl.clusters.smartenergy as smartenergy -from homeassistant.const import LENGTH_FEET, TIME_HOURS, TIME_SECONDS +from homeassistant.const import LENGTH_FEET, POWER_WATT, TIME_HOURS, TIME_SECONDS from homeassistant.core import callback from .. import registries, typing as zha_typing @@ -60,7 +60,7 @@ class Metering(ZigbeeChannel): REPORT_CONFIG = [{"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT}] unit_of_measure_map = { - 0x00: "kW", + 0x00: POWER_WATT, 0x01: f"m³/{TIME_HOURS}", 0x02: f"{LENGTH_FEET}³/{TIME_HOURS}", 0x03: f"ccf/{TIME_HOURS}", @@ -135,6 +135,12 @@ class Metering(ZigbeeChannel): def formatter_function(self, value): """Return formatted value for display.""" + if self.unit_of_measurement == POWER_WATT: + # Zigbee spec power unit is kW, but we show the value in W + value_watt = value * 1000 + if value_watt < 100: + return round(value_watt, 1) + return round(value_watt) return self._format_spec.format(value).lstrip() From 757d05a74ead56bd897ab845fe68b7bd09566ae4 Mon Sep 17 00:00:00 2001 From: Tim Messerschmidt Date: Fri, 17 Jul 2020 13:04:12 +0200 Subject: [PATCH 16/33] Fix: Passes secure parameter when setting up Nuki (#36844) (#37932) * Passes secure parameter when setting up Nuki (#36844) * Adds an additional configuration option for soft bridges instead of passing True when setting up the bridge * Revert "Adds an additional configuration option for soft bridges instead of passing True when setting up the bridge" This reverts commit af1d839ab1c130535c5a31ee60e35d3b2c151c5b. --- homeassistant/components/nuki/lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 13825cede94..f7414d54802 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -51,7 +51,7 @@ LOCK_N_GO_SERVICE_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nuki lock platform.""" bridge = NukiBridge( - config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT, + config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], True, DEFAULT_TIMEOUT, ) devices = [NukiLockEntity(lock) for lock in bridge.locks] From a3429848a2320085c327e384daeeadeaee944dc6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 17 Jul 2020 20:18:53 -0500 Subject: [PATCH 17/33] Fix Sonos speaker lookup for Plex (#37942) --- homeassistant/components/plex/__init__.py | 6 +++--- homeassistant/components/sonos/__init__.py | 10 ++++++---- tests/components/plex/mock_classes.py | 8 ++++---- tests/components/plex/test_playback.py | 12 ++++++------ 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 01f80ed0d2b..4556422dd00 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -215,7 +215,7 @@ def play_on_sonos(hass, service_call): sonos = hass.components.sonos try: - sonos_id = sonos.get_coordinator_id(entity_id) + sonos_name = sonos.get_coordinator_name(entity_id) except HomeAssistantError as err: _LOGGER.error("Cannot get Sonos device: %s", err) return @@ -239,10 +239,10 @@ def play_on_sonos(hass, service_call): else: plex_server = next(iter(plex_servers)) - sonos_speaker = plex_server.account.sonos_speaker_by_id(sonos_id) + sonos_speaker = plex_server.account.sonos_speaker(sonos_name) if sonos_speaker is None: _LOGGER.error( - "Sonos speaker '%s' could not be found on this Plex account", sonos_id + "Sonos speaker '%s' could not be found on this Plex account", sonos_name ) return diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index f19816e865d..cc33134c810 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -58,8 +58,10 @@ async def async_setup_entry(hass, entry): @bind_hass -def get_coordinator_id(hass, entity_id): - """Obtain the unique_id of a device's coordinator. +def get_coordinator_name(hass, entity_id): + """Obtain the room/name of a device's coordinator. + + Used by the Plex integration. This function is safe to run inside the event loop. """ @@ -71,5 +73,5 @@ def get_coordinator_id(hass, entity_id): ) if device.is_coordinator: - return device.unique_id - return device.coordinator.unique_id + return device.name + return device.coordinator.name diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 3812e9c87b9..eacee6d9f98 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -78,9 +78,9 @@ class MockPlexAccount: """Mock the PlexAccount resources listing method.""" return self._resources - def sonos_speaker_by_id(self, machine_identifier): + def sonos_speaker(self, speaker_name): """Mock the PlexAccount Sonos lookup method.""" - return MockPlexSonosClient(machine_identifier) + return MockPlexSonosClient(speaker_name) class MockPlexSystemAccount: @@ -378,9 +378,9 @@ class MockPlexMediaTrack(MockPlexMediaItem): class MockPlexSonosClient: """Mock a PlexSonosClient instance.""" - def __init__(self, machine_identifier): + def __init__(self, name): """Initialize the object.""" - self.machineIdentifier = machine_identifier + self.name = name def playMedia(self, item): """Mock the playMedia method.""" diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index dafc8720ab1..82682ea0ac2 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -39,7 +39,7 @@ async def test_sonos_playback(hass): # Test Sonos integration lookup failure with patch.object( - hass.components.sonos, "get_coordinator_id", side_effect=HomeAssistantError + hass.components.sonos, "get_coordinator_name", side_effect=HomeAssistantError ): assert await hass.services.async_call( DOMAIN, @@ -55,7 +55,7 @@ async def test_sonos_playback(hass): # Test success with dict with patch.object( hass.components.sonos, - "get_coordinator_id", + "get_coordinator_name", return_value="media_player.sonos_kitchen", ), patch("plexapi.playqueue.PlayQueue.create"): assert await hass.services.async_call( @@ -72,7 +72,7 @@ async def test_sonos_playback(hass): # Test success with plex_key with patch.object( hass.components.sonos, - "get_coordinator_id", + "get_coordinator_name", return_value="media_player.sonos_kitchen", ), patch("plexapi.playqueue.PlayQueue.create"): assert await hass.services.async_call( @@ -89,7 +89,7 @@ async def test_sonos_playback(hass): # Test invalid Plex server requested with patch.object( hass.components.sonos, - "get_coordinator_id", + "get_coordinator_name", return_value="media_player.sonos_kitchen", ): assert await hass.services.async_call( @@ -105,10 +105,10 @@ async def test_sonos_playback(hass): # Test no speakers available with patch.object( - loaded_server.account, "sonos_speaker_by_id", return_value=None + loaded_server.account, "sonos_speaker", return_value=None ), patch.object( hass.components.sonos, - "get_coordinator_id", + "get_coordinator_name", return_value="media_player.sonos_kitchen", ): assert await hass.services.async_call( From 28f78c0c674b9ca6a856d61e2f73ceb0fc38a062 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 18 Jul 2020 14:47:32 -0400 Subject: [PATCH 18/33] Force updates for ZHA light group entity members (#37961) * Force updates for ZHA light group entity members * add a 3 second debouncer to the forced refresh * lint --- homeassistant/components/zha/light.py | 36 ++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index efe95ae6604..f1bae3dd4c2 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,4 +1,5 @@ """Lights on Zigbee Home Automation networks.""" +import asyncio from collections import Counter from datetime import timedelta import functools @@ -31,6 +32,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import State, callback +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util @@ -406,7 +408,7 @@ class Light(BaseLight, ZhaEntity): async def async_get_state(self, from_cache=True): """Attempt to retrieve on off state from the light.""" - self.debug("polling current state") + self.debug("polling current state - from cache: %s", from_cache) if self._on_off_channel: state = await self._on_off_channel.get_attribute_value( "on_off", from_cache=from_cache @@ -494,6 +496,30 @@ class LightGroup(BaseLight, ZhaGroupEntity): self._level_channel = group.endpoint[LevelControl.cluster_id] self._color_channel = group.endpoint[Color.cluster_id] self._identify_channel = group.endpoint[Identify.cluster_id] + self._debounced_member_refresh = None + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + if self._debounced_member_refresh is None: + force_refresh_debouncer = Debouncer( + self.hass, + _LOGGER, + cooldown=3, + immediate=True, + function=self._force_member_updates, + ) + self._debounced_member_refresh = force_refresh_debouncer + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + await super().async_turn_on(**kwargs) + await self._debounced_member_refresh.async_call() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + await super().async_turn_off(**kwargs) + await self._debounced_member_refresh.async_call() async def async_update(self) -> None: """Query all members and determine the light group state.""" @@ -541,3 +567,11 @@ class LightGroup(BaseLight, ZhaGroupEntity): # Bitwise-and the supported features with the GroupedLight's features # so that we don't break in the future when a new feature is added. self._supported_features &= SUPPORT_GROUP_LIGHT + + async def _force_member_updates(self): + """Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" + component = self.hass.data[light.DOMAIN] + entities = [component.get_entity(entity_id) for entity_id in self._entity_ids] + tasks = [entity.async_get_state(from_cache=False) for entity in entities] + if tasks: + await asyncio.gather(*tasks) From 27859a278458ef4dd79d2d024bb8f0b9d6da293b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 18 Jul 2020 23:37:37 +0000 Subject: [PATCH 19/33] Bumped version to 0.113.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cf3e1c4cac5..4cf67fc1aba 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 8409385fca3bf73ea1aeed17d54729d559222341 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 19 Jul 2020 12:36:59 +0200 Subject: [PATCH 20/33] Bump pychromecast to 7.1.2 (#37976) --- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 2 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b0d49681414..5d807525226 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.0.1"], + "requirements": ["pychromecast==7.1.2"], "after_dependencies": ["cloud","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 21b7d207580..44b6bf451c1 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -279,6 +279,8 @@ class CastDevice(MediaPlayerEntity): cast_info.uuid, cast_info.model_name, cast_info.friendly_name, + None, + None, ), ChromeCastZeroconf.get_zeroconf(), ) diff --git a/requirements_all.txt b/requirements_all.txt index 2d2e22f5983..ef1e9481764 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1244,7 +1244,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.0.1 +pychromecast==7.1.2 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecf468ca6b4..4bcd7d8cb8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -577,7 +577,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==7.0.1 +pychromecast==7.1.2 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 From 9b46796969bfefc62beeb03f830c4c19887bc717 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 19 Jul 2020 18:05:53 -0400 Subject: [PATCH 21/33] Force updates for ZHA light group entity members (Part 2) (#37995) --- homeassistant/components/zha/light.py | 29 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index f1bae3dd4c2..6fefc795460 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,5 +1,4 @@ """Lights on Zigbee Home Automation networks.""" -import asyncio from collections import Counter from datetime import timedelta import functools @@ -33,7 +32,10 @@ from homeassistant.components.light import ( from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import State, callback from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util @@ -73,6 +75,7 @@ UNSUPPORTED_ATTRIBUTE = 0x86 STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, light.DOMAIN) PARALLEL_UPDATES = 0 +SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" SUPPORT_GROUP_LIGHT = ( SUPPORT_BRIGHTNESS @@ -380,6 +383,12 @@ class Light(BaseLight, ZhaEntity): self._cancel_refresh_handle = async_track_time_interval( self.hass, self._refresh, timedelta(seconds=refresh_interval) ) + await self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_STATE_CHANGED, + self._maybe_force_refresh, + signal_override=True, + ) async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" @@ -470,6 +479,12 @@ class Light(BaseLight, ZhaEntity): await self.async_get_state(from_cache=False) self.async_write_ha_state() + async def _maybe_force_refresh(self, signal): + """Force update the state if the signal contains the entity id for this entity.""" + if self.entity_id in signal["entity_ids"]: + await self.async_get_state(from_cache=False) + self.async_write_ha_state() + @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, @@ -570,8 +585,8 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def _force_member_updates(self): """Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" - component = self.hass.data[light.DOMAIN] - entities = [component.get_entity(entity_id) for entity_id in self._entity_ids] - tasks = [entity.async_get_state(from_cache=False) for entity in entities] - if tasks: - await asyncio.gather(*tasks) + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_STATE_CHANGED, + {"entity_ids": self._entity_ids}, + ) From 0b32caa71fb1560fbfd02f0c60361b61c4d43125 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Jul 2020 00:35:25 +0000 Subject: [PATCH 22/33] Bumped version to 0.113.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4cf67fc1aba..a041aa940e0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 66e490e55fb4d3e4beeaf97f828afc6ad588cd02 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 22 Jul 2020 15:27:04 +0200 Subject: [PATCH 23/33] Rfxtrx fixup restore (#38039) --- homeassistant/components/rfxtrx/__init__.py | 46 +++++---- .../components/rfxtrx/binary_sensor.py | 96 +++---------------- homeassistant/components/rfxtrx/cover.py | 23 ++--- homeassistant/components/rfxtrx/light.py | 23 +++-- homeassistant/components/rfxtrx/sensor.py | 56 +---------- homeassistant/components/rfxtrx/switch.py | 23 +++-- tests/components/rfxtrx/test_binary_sensor.py | 27 +++++- tests/components/rfxtrx/test_cover.py | 23 +++++ tests/components/rfxtrx/test_light.py | 25 +++++ tests/components/rfxtrx/test_sensor.py | 27 ++++++ tests/components/rfxtrx/test_switch.py | 21 ++++ 11 files changed, 204 insertions(+), 186 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 54863f86332..1f4655aedc7 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( UNIT_PERCENTAGE, UV_INDEX, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -343,35 +344,30 @@ def get_device_id(device, data_bits=None): return (f"{device.packettype:x}", f"{device.subtype:x}", id_string) -class RfxtrxDevice(RestoreEntity): +class RfxtrxEntity(RestoreEntity): """Represents a Rfxtrx device. Contains the common logic for Rfxtrx lights and switches. """ - def __init__(self, device, device_id, signal_repetitions, event=None): + def __init__(self, device, device_id, event=None): """Initialize the device.""" - self.signal_repetitions = signal_repetitions self._name = f"{device.type_string} {device.id_string}" self._device = device - self._event = None - self._state = None + self._event = event self._device_id = device_id self._unique_id = "_".join(x for x in self._device_id) - if event: - self._apply_event(event) - async def async_added_to_hass(self): """Restore RFXtrx device state (ON/OFF).""" if self._event: - return + self._apply_event(self._event) - old_state = await self.async_get_last_state() - if old_state is not None: - event = old_state.attributes.get(ATTR_EVENT) - if event: - self._apply_event(get_rfx_object(event)) + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_EVENT, self._handle_event + ) + ) @property def should_poll(self): @@ -390,11 +386,6 @@ class RfxtrxDevice(RestoreEntity): return None return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - @property - def is_on(self): - """Return true if device is on.""" - return self._state - @property def assumed_state(self): """Return true if unable to access real state of entity.""" @@ -418,6 +409,23 @@ class RfxtrxDevice(RestoreEntity): """Apply a received event.""" self._event = event + @callback + def _handle_event(self, event, device_id): + """Handle a reception of data, overridden by other classes.""" + + +class RfxtrxCommandEntity(RfxtrxEntity): + """Represents a Rfxtrx device. + + Contains the common logic for Rfxtrx lights and switches. + """ + + def __init__(self, device, device_id, signal_repetitions=1, event=None): + """Initialzie a switch or light device.""" + super().__init__(device, device_id, event=event) + self.signal_repetitions = signal_repetitions + self._state = None + def _send_command(self, command, brightness=0): rfx_object = self.hass.data[DATA_RFXOBJECT] diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 5f6e32437c4..3f0010b139e 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -12,14 +12,13 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import event as evt -from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_OFF_DELAY, - DOMAIN, SIGNAL_EVENT, + RfxtrxEntity, find_possible_pt2262_device, get_device_id, get_pt2262_cmd, @@ -107,7 +106,7 @@ async def async_setup_entry( ) -class RfxtrxBinarySensor(BinarySensorEntity, RestoreEntity): +class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """A representation of a RFXtrx binary sensor.""" def __init__( @@ -122,25 +121,17 @@ class RfxtrxBinarySensor(BinarySensorEntity, RestoreEntity): event=None, ): """Initialize the RFXtrx sensor.""" - self._event = None - self._device = device - self._name = f"{device.type_string} {device.id_string}" + super().__init__(device, device_id, event=event) self._device_class = device_class self._data_bits = data_bits self._off_delay = off_delay - self._state = False - self.delay_listener = None + self._state = None + self._delay_listener = None self._cmd_on = cmd_on self._cmd_off = cmd_off - self._device_id = device_id - self._unique_id = "_".join(x for x in self._device_id) - - if event: - self._apply_event(event) - async def async_added_to_hass(self): - """Restore RFXtrx switch device state (ON/OFF).""" + """Restore device state.""" await super().async_added_to_hass() if self._event is None: @@ -150,44 +141,6 @@ class RfxtrxBinarySensor(BinarySensorEntity, RestoreEntity): if event: self._apply_event(get_rfx_object(event)) - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_EVENT, self._handle_event - ) - ) - - @property - def name(self): - """Return the device name.""" - return self._name - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - if not self._event: - return None - return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - - @property - def data_bits(self): - """Return the number of data bits.""" - return self._data_bits - - @property - def cmd_on(self): - """Return the value of the 'On' command.""" - return self._cmd_on - - @property - def cmd_off(self): - """Return the value of the 'Off' command.""" - return self._cmd_off - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def force_update(self) -> bool: """We should force updates. Repeated states have meaning.""" @@ -198,38 +151,19 @@ class RfxtrxBinarySensor(BinarySensorEntity, RestoreEntity): """Return the sensor class.""" return self._device_class - @property - def off_delay(self): - """Return the off_delay attribute value.""" - return self._off_delay - @property def is_on(self): """Return true if the sensor state is True.""" return self._state - @property - def unique_id(self): - """Return unique identifier of remote device.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, *self._device_id)}, - "name": f"{self._device.type_string} {self._device.id_string}", - "model": self._device.type_string, - } - def _apply_event_lighting4(self, event): """Apply event for a lighting 4 device.""" - if self.data_bits is not None: - cmd = get_pt2262_cmd(event.device.id_string, self.data_bits) + if self._data_bits is not None: + cmd = get_pt2262_cmd(event.device.id_string, self._data_bits) cmd = int(cmd, 16) - if cmd == self.cmd_on: + if cmd == self._cmd_on: self._state = True - elif cmd == self.cmd_off: + elif cmd == self._cmd_off: self._state = False else: self._state = True @@ -242,7 +176,7 @@ class RfxtrxBinarySensor(BinarySensorEntity, RestoreEntity): def _apply_event(self, event): """Apply command from rfxtrx.""" - self._event = event + super()._apply_event(event) if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: self._apply_event_lighting4(event) else: @@ -265,15 +199,15 @@ class RfxtrxBinarySensor(BinarySensorEntity, RestoreEntity): self.async_write_ha_state() - if self.is_on and self.off_delay is not None and self.delay_listener is None: + if self.is_on and self._off_delay is not None and self._delay_listener is None: @callback def off_delay_listener(now): """Switch device off after a delay.""" - self.delay_listener = None + self._delay_listener = None self._state = False self.async_write_ha_state() - self.delay_listener = evt.async_call_later( - self.hass, self.off_delay.total_seconds(), off_delay_listener + self._delay_listener = evt.async_call_later( + self.hass, self._off_delay.total_seconds(), off_delay_listener ) diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index a3cefb42cb7..af5c48810ee 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -2,16 +2,15 @@ import logging from homeassistant.components.cover import CoverEntity -from homeassistant.const import CONF_DEVICES +from homeassistant.const import CONF_DEVICES, STATE_OPEN from homeassistant.core import callback -from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, SIGNAL_EVENT, - RfxtrxDevice, + RfxtrxCommandEntity, get_device_id, get_rfx_object, ) @@ -79,23 +78,17 @@ async def async_setup_entry( hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, cover_update) -class RfxtrxCover(RfxtrxDevice, CoverEntity, RestoreEntity): +class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): """Representation of a RFXtrx cover.""" async def async_added_to_hass(self): - """Restore RFXtrx cover device state (OPEN/CLOSE).""" + """Restore device state.""" await super().async_added_to_hass() - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_EVENT, self._handle_event - ) - ) - - @property - def should_poll(self): - """Return the polling state. No polling available in RFXtrx cover.""" - return False + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_OPEN @property def is_closed(self): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 649be7be3fe..71bf54d3d50 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, LightEntity, ) -from homeassistant.const import CONF_DEVICES +from homeassistant.const import CONF_DEVICES, STATE_ON from homeassistant.core import callback from . import ( @@ -16,7 +16,7 @@ from . import ( CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, SIGNAL_EVENT, - RfxtrxDevice, + RfxtrxCommandEntity, get_device_id, get_rfx_object, ) @@ -48,7 +48,7 @@ async def async_setup_entry( _LOGGER.error("Invalid device: %s", packet_id) continue if not supported(event): - return + continue device_id = get_device_id(event.device) if device_id in device_ids: @@ -92,7 +92,7 @@ async def async_setup_entry( hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, light_update) -class RfxtrxLight(RfxtrxDevice, LightEntity): +class RfxtrxLight(RfxtrxCommandEntity, LightEntity): """Representation of a RFXtrx light.""" _brightness = 0 @@ -101,11 +101,11 @@ class RfxtrxLight(RfxtrxDevice, LightEntity): """Restore RFXtrx device state (ON/OFF).""" await super().async_added_to_hass() - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_EVENT, self._handle_event - ) - ) + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + self._brightness = old_state.attributes.get(ATTR_BRIGHTNESS) @property def brightness(self): @@ -117,6 +117,11 @@ class RfxtrxLight(RfxtrxDevice, LightEntity): """Flag supported features.""" return SUPPORT_RFXTRX + @property + def is_on(self): + """Return true if device is on.""" + return self._state + def turn_on(self, **kwargs): """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index de341307551..537fabd7aa7 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -11,13 +11,12 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import CONF_DEVICES from homeassistant.core import callback -from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, DATA_TYPES, - DOMAIN, SIGNAL_EVENT, + RfxtrxEntity, get_device_id, get_rfx_object, ) @@ -113,27 +112,22 @@ async def async_setup_entry( hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, sensor_update) -class RfxtrxSensor(RestoreEntity): +class RfxtrxSensor(RfxtrxEntity): """Representation of a RFXtrx sensor.""" def __init__(self, device, device_id, data_type, event=None): """Initialize the sensor.""" - self._event = None - self._device = device - self._name = f"{device.type_string} {device.id_string} {data_type}" + super().__init__(device, device_id, event=event) self.data_type = data_type self._unit_of_measurement = DATA_TYPES.get(data_type, "") - self._device_id = device_id + self._name = f"{device.type_string} {device.id_string} {data_type}" self._unique_id = "_".join(x for x in (*self._device_id, data_type)) self._device_class = DEVICE_CLASSES.get(data_type) self._convert_fun = CONVERT_FUNCTIONS.get(data_type, lambda x: x) - if event: - self._apply_event(event) - async def async_added_to_hass(self): - """Restore RFXtrx switch device state (ON/OFF).""" + """Restore device state.""" await super().async_added_to_hass() if self._event is None: @@ -143,16 +137,6 @@ class RfxtrxSensor(RestoreEntity): if event: self._apply_event(get_rfx_object(event)) - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_EVENT, self._handle_event - ) - ) - - def __str__(self): - """Return the name of the sensor.""" - return self._name - @property def state(self): """Return the state of the sensor.""" @@ -161,18 +145,6 @@ class RfxtrxSensor(RestoreEntity): value = self._event.values.get(self.data_type) return self._convert_fun(value) - @property - def name(self): - """Get the name of the sensor.""" - return self._name - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - if not self._event: - return None - return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" @@ -193,24 +165,6 @@ class RfxtrxSensor(RestoreEntity): """Return a device class for sensor.""" return self._device_class - @property - def unique_id(self): - """Return unique identifier of remote device.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, *self._device_id)}, - "name": f"{self._device.type_string} {self._device.id_string}", - "model": self._device.type_string, - } - - def _apply_event(self, event): - """Apply command from rfxtrx.""" - self._event = event - @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 7b2a23c1624..e5c96215c83 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -4,9 +4,8 @@ import logging import RFXtrx as rfxtrxmod from homeassistant.components.switch import SwitchEntity -from homeassistant.const import CONF_DEVICES +from homeassistant.const import CONF_DEVICES, STATE_ON from homeassistant.core import callback -from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -14,7 +13,7 @@ from . import ( DEFAULT_SIGNAL_REPETITIONS, DOMAIN, SIGNAL_EVENT, - RfxtrxDevice, + RfxtrxCommandEntity, get_device_id, get_rfx_object, ) @@ -89,18 +88,17 @@ async def async_setup_entry( hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, switch_update) -class RfxtrxSwitch(RfxtrxDevice, SwitchEntity, RestoreEntity): +class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): """Representation of a RFXtrx switch.""" async def async_added_to_hass(self): - """Restore RFXtrx switch device state (ON/OFF).""" + """Restore device state.""" await super().async_added_to_hass() - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_EVENT, self._handle_event - ) - ) + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON def _apply_event(self, event): """Apply command from rfxtrx.""" @@ -120,6 +118,11 @@ class RfxtrxSwitch(RfxtrxDevice, SwitchEntity, RestoreEntity): self.async_write_ha_state() + @property + def is_on(self): + """Return true if device is on.""" + return self._state + def turn_on(self, **kwargs): """Turn the device on.""" self._send_command("turn_on") diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index d694dcb34cf..ad04a0763f2 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -1,12 +1,16 @@ """The tests for the Rfxtrx sensor platform.""" from datetime import timedelta +import pytest + +from homeassistant.components.rfxtrx.const import ATTR_EVENT +from homeassistant.core import State from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from . import _signal_event -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache async def test_one(hass, rfxtrx): @@ -59,6 +63,27 @@ async def test_one_pt2262(hass, rfxtrx): assert state.state == "off" +@pytest.mark.parametrize( + "state,event", + [["on", "0b1100cd0213c7f230010f71"], ["off", "0b1100cd0213c7f230000f71"]], +) +async def test_state_restore(hass, rfxtrx, state, event): + """State restoration.""" + + entity_id = "binary_sensor.ac_213c7f2_48" + + mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})]) + + assert await async_setup_component( + hass, + "rfxtrx", + {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f230010f71": {}}}}, + ) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == state + + async def test_several(hass, rfxtrx): """Test with 3.""" assert await async_setup_component( diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index 9a9b37f4e36..73c3cb9cc27 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -1,10 +1,15 @@ """The tests for the Rfxtrx cover platform.""" from unittest.mock import call +import pytest + +from homeassistant.core import State from homeassistant.setup import async_setup_component from . import _signal_event +from tests.common import mock_restore_cache + async def test_one_cover(hass, rfxtrx): """Test with 1 cover.""" @@ -46,6 +51,24 @@ async def test_one_cover(hass, rfxtrx): ] +@pytest.mark.parametrize("state", ["open", "closed"]) +async def test_state_restore(hass, rfxtrx, state): + """State restoration.""" + + entity_id = "cover.lightwaverf_siemens_0213c7_242" + + mock_restore_cache(hass, [State(entity_id, state)]) + + assert await async_setup_component( + hass, + "rfxtrx", + {"rfxtrx": {"device": "abcd", "devices": {"0b1400cd0213c7f20d010f51": {}}}}, + ) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == state + + async def test_several_covers(hass, rfxtrx): """Test with 3 covers.""" assert await async_setup_component( diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index 53ddf8bc48c..b96dec95e6e 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -3,10 +3,14 @@ from unittest.mock import call import pytest +from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.core import State from homeassistant.setup import async_setup_component from . import _signal_event +from tests.common import mock_restore_cache + async def test_one_light(hass, rfxtrx): """Test with 1 light.""" @@ -83,6 +87,27 @@ async def test_one_light(hass, rfxtrx): ] +@pytest.mark.parametrize("state,brightness", [["on", 100], ["on", 50], ["off", None]]) +async def test_state_restore(hass, rfxtrx, state, brightness): + """State restoration.""" + + entity_id = "light.ac_213c7f2_16" + + mock_restore_cache( + hass, [State(entity_id, state, attributes={ATTR_BRIGHTNESS: brightness})] + ) + + assert await async_setup_component( + hass, + "rfxtrx", + {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210020f51": {}}}}, + ) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == state + assert hass.states.get(entity_id).attributes.get(ATTR_BRIGHTNESS) == brightness + + async def test_several_lights(hass, rfxtrx): """Test with 3 lights.""" assert await async_setup_component( diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 91e010f9e54..3a797f4168d 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -1,9 +1,15 @@ """The tests for the Rfxtrx sensor platform.""" +import pytest + +from homeassistant.components.rfxtrx.const import ATTR_EVENT from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.core import State from homeassistant.setup import async_setup_component from . import _signal_event +from tests.common import mock_restore_cache + async def test_default_config(hass, rfxtrx): """Test with 0 sensor.""" @@ -34,6 +40,27 @@ async def test_one_sensor(hass, rfxtrx): assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS +@pytest.mark.parametrize( + "state,event", + [["18.4", "0a520801070100b81b0279"], ["17.9", "0a52085e070100b31b0279"]], +) +async def test_state_restore(hass, rfxtrx, state, event): + """State restoration.""" + + entity_id = "sensor.wt260_wt260h_wt440h_wt450_wt450h_07_01_temperature" + + mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})]) + + assert await async_setup_component( + hass, + "rfxtrx", + {"rfxtrx": {"device": "abcd", "devices": {"0a520801070100b81b0279": {}}}}, + ) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == state + + async def test_one_sensor_no_datatype(hass, rfxtrx): """Test with 1 sensor.""" assert await async_setup_component( diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 24d1cbbcdf0..22f7a73c77c 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -3,10 +3,13 @@ from unittest.mock import call import pytest +from homeassistant.core import State from homeassistant.setup import async_setup_component from . import _signal_event +from tests.common import mock_restore_cache + async def test_one_switch(hass, rfxtrx): """Test with 1 switch.""" @@ -42,6 +45,24 @@ async def test_one_switch(hass, rfxtrx): ] +@pytest.mark.parametrize("state", ["on", "off"]) +async def test_state_restore(hass, rfxtrx, state): + """State restoration.""" + + entity_id = "switch.ac_213c7f2_16" + + mock_restore_cache(hass, [State(entity_id, state)]) + + assert await async_setup_component( + hass, + "rfxtrx", + {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210010f51": {}}}}, + ) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == state + + async def test_several_switches(hass, rfxtrx): """Test with 3 switches.""" assert await async_setup_component( From 98dd56eed5511c1218e616a73ce303043a2ee210 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sun, 19 Jul 2020 05:52:46 -0400 Subject: [PATCH 24/33] Make nested get() statements safe (#37965) --- .../components/environment_canada/weather.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 7bc614bd09e..78ede4dbc5e 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -99,7 +99,7 @@ class ECWeather(WeatherEntity): @property def temperature(self): """Return the temperature.""" - if self.ec_data.conditions.get("temperature").get("value"): + if self.ec_data.conditions.get("temperature", {}).get("value"): return float(self.ec_data.conditions["temperature"]["value"]) if self.ec_data.hourly_forecasts[0].get("temperature"): return float(self.ec_data.hourly_forecasts[0]["temperature"]) @@ -113,35 +113,35 @@ class ECWeather(WeatherEntity): @property def humidity(self): """Return the humidity.""" - if self.ec_data.conditions.get("humidity").get("value"): + if self.ec_data.conditions.get("humidity", {}).get("value"): return float(self.ec_data.conditions["humidity"]["value"]) return None @property def wind_speed(self): """Return the wind speed.""" - if self.ec_data.conditions.get("wind_speed").get("value"): + if self.ec_data.conditions.get("wind_speed", {}).get("value"): return float(self.ec_data.conditions["wind_speed"]["value"]) return None @property def wind_bearing(self): """Return the wind bearing.""" - if self.ec_data.conditions.get("wind_bearing").get("value"): + if self.ec_data.conditions.get("wind_bearing", {}).get("value"): return float(self.ec_data.conditions["wind_bearing"]["value"]) return None @property def pressure(self): """Return the pressure.""" - if self.ec_data.conditions.get("pressure").get("value"): + if self.ec_data.conditions.get("pressure", {}).get("value"): return 10 * float(self.ec_data.conditions["pressure"]["value"]) return None @property def visibility(self): """Return the visibility.""" - if self.ec_data.conditions.get("visibility").get("value"): + if self.ec_data.conditions.get("visibility", {}).get("value"): return float(self.ec_data.conditions["visibility"]["value"]) return None @@ -150,7 +150,7 @@ class ECWeather(WeatherEntity): """Return the weather condition.""" icon_code = None - if self.ec_data.conditions.get("icon_code").get("value"): + if self.ec_data.conditions.get("icon_code", {}).get("value"): icon_code = self.ec_data.conditions["icon_code"]["value"] elif self.ec_data.hourly_forecasts[0].get("icon_code"): icon_code = self.ec_data.hourly_forecasts[0]["icon_code"] From b003d9675c6af8ed844d585b8f3fcef25bf4bcbf Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 20 Jul 2020 10:23:59 -0400 Subject: [PATCH 25/33] Fix issue with Insteon events not firing correctly (#37974) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index d1a31117fb9..cdcd07a403b 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,6 +2,6 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["pyinsteon==1.0.5"], + "requirements": ["pyinsteon==1.0.7"], "codeowners": ["@teharris1"] } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index ef1e9481764..f2d60e45b2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1380,7 +1380,7 @@ pyialarm==0.3 pyicloud==0.9.7 # homeassistant.components.insteon -pyinsteon==1.0.5 +pyinsteon==1.0.7 # homeassistant.components.intesishome pyintesishome==1.7.5 From 34751fcd86e8ae526e7447ada85013e2eb5e5bfc Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Mon, 20 Jul 2020 00:55:50 -0500 Subject: [PATCH 26/33] Fix notify.slack service calls using data_template (#37980) --- homeassistant/components/slack/notify.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index aec7de1bd88..b7f3d81feb0 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -224,7 +224,10 @@ class SlackNotificationService(BaseNotificationService): async def async_send_message(self, message, **kwargs): """Send a message to Slack.""" - data = kwargs.get(ATTR_DATA, {}) + data = kwargs.get(ATTR_DATA) + + if data is None: + data = {} try: DATA_SCHEMA(data) From 0cd8dce9df1ec72d71a014205f6ccfcb15c2181b Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Mon, 20 Jul 2020 22:00:11 -0700 Subject: [PATCH 27/33] Check if robot has boundaries to update (#38030) --- homeassistant/components/neato/vacuum.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 2f5703615b6..841b160ad30 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -264,12 +264,13 @@ class NeatoConnectedVacuum(StateVacuumEntity): maps["name"], robot_boundaries, ) - self._robot_boundaries += robot_boundaries["data"]["boundaries"] - _LOGGER.debug( - "List of boundaries for '%s': %s", - self.entity_id, - self._robot_boundaries, - ) + if "boundaries" in robot_boundaries["data"]: + self._robot_boundaries += robot_boundaries["data"]["boundaries"] + _LOGGER.debug( + "List of boundaries for '%s': %s", + self.entity_id, + self._robot_boundaries, + ) @property def name(self): From d8d48b0a21efd95f3716e52925e1a6bda8a5ad9b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Jul 2020 21:36:21 +0200 Subject: [PATCH 28/33] Correct arguments to MQTT will_set (#38036) --- homeassistant/components/mqtt/__init__.py | 3 ++- tests/components/mqtt/test_init.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index b88c536d6a3..dac6527b268 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -716,7 +716,8 @@ class MQTT: self._mqttc.will_set( # pylint: disable=no-value-for-parameter *attr.astuple( will_message, - filter=lambda attr, value: attr.name != "subscribed_topic", + filter=lambda attr, value: attr.name + not in ["subscribed_topic", "timestamp"], ) ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 3dee1dc874b..a6eb7e46f59 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -799,13 +799,13 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): ) async def test_custom_will_message(hass, mqtt_client_mock, mqtt_mock): """Test will message.""" - mqtt_client_mock.will_set.assert_called_with("death", "death", 0, False, None) + mqtt_client_mock.will_set.assert_called_with("death", "death", 0, False) async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): """Test will message.""" mqtt_client_mock.will_set.assert_called_with( - "homeassistant/status", "offline", 0, False, None + "homeassistant/status", "offline", 0, False ) From 9f12226b2b6e39774823f9ed4d54cc6be737f5aa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Jul 2020 23:10:34 +0200 Subject: [PATCH 29/33] Use keywords for MQTT birth and will (#38040) --- homeassistant/components/mqtt/__init__.py | 18 ++++++++---------- tests/components/mqtt/test_init.py | 6 ++++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index dac6527b268..a0527cfe427 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -714,11 +714,10 @@ class MQTT: if will_message is not None: self._mqttc.will_set( # pylint: disable=no-value-for-parameter - *attr.astuple( - will_message, - filter=lambda attr, value: attr.name - not in ["subscribed_topic", "timestamp"], - ) + topic=will_message.topic, + payload=will_message.payload, + qos=will_message.qos, + retain=will_message.retain, ) async def async_publish( @@ -865,11 +864,10 @@ class MQTT: birth_message = Message(**self.conf[CONF_BIRTH_MESSAGE]) self.hass.add_job( self.async_publish( # pylint: disable=no-value-for-parameter - *attr.astuple( - birth_message, - filter=lambda attr, value: attr.name - not in ["subscribed_topic", "timestamp"], - ) + topic=birth_message.topic, + payload=birth_message.payload, + qos=birth_message.qos, + retain=birth_message.retain, ) ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a6eb7e46f59..15d92b9a311 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -799,13 +799,15 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): ) async def test_custom_will_message(hass, mqtt_client_mock, mqtt_mock): """Test will message.""" - mqtt_client_mock.will_set.assert_called_with("death", "death", 0, False) + mqtt_client_mock.will_set.assert_called_with( + topic="death", payload="death", qos=0, retain=False + ) async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): """Test will message.""" mqtt_client_mock.will_set.assert_called_with( - "homeassistant/status", "offline", 0, False + topic="homeassistant/status", payload="offline", qos=0, retain=False ) From 6617d676e569f864e4933e1fe7cdf141b27b6328 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 21 Jul 2020 17:42:23 -0400 Subject: [PATCH 30/33] ZHA dependencies bump bellows to 0.18.0 (#38043) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 24d9a0a3962..974cac32c33 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.17.0", + "bellows==0.18.0", "pyserial==3.4", "zha-quirks==0.0.42", "zigpy-cc==0.4.4", diff --git a/requirements_all.txt b/requirements_all.txt index f2d60e45b2a..2492a229806 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -319,7 +319,7 @@ beautifulsoup4==4.9.0 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows==0.17.0 +bellows==0.18.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bcd7d8cb8a..02595d9f708 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -166,7 +166,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.17.0 +bellows==0.18.0 # homeassistant.components.blebox blebox_uniapi==1.3.2 From a50cf1d00aa654e419d554a36fc65f58c80aeb21 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 21 Jul 2020 19:19:32 -0700 Subject: [PATCH 31/33] Add MQTT to constraints file (#38049) --- homeassistant/package_constraints.txt | 1 + script/gen_requirements_all.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 972c80ea6bc..31fdad0d54a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,6 +17,7 @@ home-assistant-frontend==20200716.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 netdisco==2.8.0 +paho-mqtt==1.5.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2020.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d3e4d5c63fc..4625924da29 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -297,8 +297,11 @@ def gather_constraints(): return ( "\n".join( sorted( - core_requirements() - + list(gather_recursive_requirements("default_config")) + { + *core_requirements(), + *gather_recursive_requirements("default_config"), + *gather_recursive_requirements("mqtt"), + } ) + [""] ) From dfd956b0838c92aa69cfbd43bdbb223bf94df7ea Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 22 Jul 2020 13:08:31 +0200 Subject: [PATCH 32/33] Fix rfxtrx stop after first non light (#38057) From c505bf2df2e7f2df7e2c86b56ead570b347850b9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jul 2020 15:48:28 +0200 Subject: [PATCH 33/33] Bumped version to 0.113.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a041aa940e0..1a4c1941e63 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1)