diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c2846f8c57f..22711b84b9d 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==76"], + "requirements": ["pydeconz==77"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index c8341a3ec2c..31085292fbb 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.7", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.8", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 2a51c6ffd83..02a60049f07 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,6 +2,6 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.2.4"], + "requirements": ["env_canada==0.2.5"], "codeowners": ["@michaeldavie"] } diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 486e9f1643c..d188dd270ab 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.0.0", + "HAP-python==3.1.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index fdf48ebba5d..6c945c81115 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -248,4 +248,5 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity): ENTITY_TYPES = { ServicesTypes.GARAGE_DOOR_OPENER: HomeKitGarageDoorCover, ServicesTypes.WINDOW_COVERING: HomeKitWindowCover, + ServicesTypes.WINDOW: HomeKitWindowCover, } diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index db0df3a073b..bf1725a036a 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -52,7 +52,7 @@ async def async_setup_entry(hass, entry): ) websession = aiohttp_client.async_get_clientsession(hass) - client = Client(entry.data[CONF_ZIP_CODE], websession) + client = Client(entry.data[CONF_ZIP_CODE], session=websession) async def async_get_data_from_api(api_coro): """Get data from a particular API coroutine.""" diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index e43c61985d6..ecd1e3c3c4b 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -30,7 +30,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): websession = aiohttp_client.async_get_clientsession(self.hass) try: - Client(user_input[CONF_ZIP_CODE], websession) + Client(user_input[CONF_ZIP_CODE], session=websession) except InvalidZipError: return self.async_show_form( step_id="user", diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 5ab331df44e..6445b4ad91f 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.19.2", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index ee3471725b6..5f863ad7f34 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.11"], + "requirements": ["pymyq==2.0.12"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 339b5750036..db78e5ce0e9 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -2,6 +2,6 @@ "domain": "nissan_leaf", "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": ["pycarwings2==2.9"], + "requirements": ["pycarwings2==2.10"], "codeowners": ["@filcole"] } diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 69dac297b1b..34c2909188f 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -161,7 +161,7 @@ class NWSWeather(WeatherEntity): temp_c = None if self.observation: temp_c = self.observation.get("temperature") - if temp_c: + if temp_c is not None: return convert_temperature(temp_c, TEMP_CELSIUS, TEMP_FAHRENHEIT) return None @@ -273,7 +273,7 @@ class NWSWeather(WeatherEntity): data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") wind_speed = forecast_entry.get("windSpeedAvg") - if wind_speed: + if wind_speed is not None: if self.is_metric: data[ATTR_FORECAST_WIND_SPEED] = round( convert_distance(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 41c56e38db6..c8697adbcd4 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -275,7 +275,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ]: hass.services.async_register(DOMAIN, service, method, schema=schema) - hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) + hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( + async_reload_entry + ) return True diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 4f5601a7f30..4bd5903253a 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", "requirements": [ - "roonapi==0.0.25" + "roonapi==0.0.28" ], "codeowners": [ "@pavoni" diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index e7dad0885a0..f06d815e5c5 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -56,7 +56,7 @@ class TriggerInstance: event_trigger.CONF_EVENT_TYPE: TASMOTA_EVENT, event_trigger.CONF_EVENT_DATA: { "mac": self.trigger.tasmota_trigger.cfg.mac, - "source": self.trigger.tasmota_trigger.cfg.source, + "source": self.trigger.tasmota_trigger.cfg.subtype, "event": self.trigger.tasmota_trigger.cfg.event, }, } @@ -126,7 +126,7 @@ class Trigger: def _on_trigger(): data = { "mac": self.tasmota_trigger.cfg.mac, - "source": self.tasmota_trigger.cfg.source, + "source": self.tasmota_trigger.cfg.subtype, "event": self.tasmota_trigger.cfg.event, } self.hass.bus.async_fire( diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 5d748edb841..80ad585486b 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -52,25 +52,33 @@ async def async_attach_trigger( if not result_as_boolean(result): return - entity_id = event.data.get("entity_id") - from_s = event.data.get("old_state") - to_s = event.data.get("new_state") + entity_id = event and event.data.get("entity_id") + from_s = event and event.data.get("old_state") + to_s = event and event.data.get("new_state") + + if entity_id is not None: + description = f"{entity_id} via template" + else: + description = "time change or manual update via template" + + template_variables = { + "platform": platform_type, + "entity_id": entity_id, + "from_state": from_s, + "to_state": to_s, + } + trigger_variables = { + "for": time_delta, + "description": description, + } @callback def call_action(*_): """Call action with right context.""" + nonlocal trigger_variables hass.async_run_hass_job( job, - { - "trigger": { - "platform": "template", - "entity_id": entity_id, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period, - "description": f"{entity_id} via template", - } - }, + {"trigger": {**template_variables, **trigger_variables}}, (to_s.context if to_s else None), ) @@ -78,18 +86,9 @@ async def async_attach_trigger( call_action() return - variables = { - "trigger": { - "platform": platform_type, - "entity_id": entity_id, - "from_state": from_s, - "to_state": to_s, - } - } - try: period = cv.positive_time_period( - template.render_complex(time_delta, variables) + template.render_complex(time_delta, {"trigger": template_variables}) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( @@ -97,6 +96,8 @@ async def async_attach_trigger( ) return + trigger_variables["for"] = period + delay_cancel = async_call_later(hass, period.seconds, call_action) info = async_track_template_result( diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 7ffa8ed24bf..57f58f05993 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.4"], + "requirements": ["pytradfri[async]==7.0.5"], "homekit": { "models": ["TRADFRI"] }, diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 89fb17affc8..69790e71732 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -204,7 +204,7 @@ class Volumio(MediaPlayerEntity): async def async_media_pause(self): """Send media_pause command to media player.""" - if self._state["trackType"] == "webradio": + if self._state.get("trackType") == "webradio": await self._volumio.stop() else: await self._volumio.pause() diff --git a/homeassistant/const.py b/homeassistant/const.py index 582648e8d32..ac4f07a7086 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2020 MINOR_VERSION = 12 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) diff --git a/requirements_all.txt b/requirements_all.txt index 3b1fa3f76e6..f1426cebca8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -17,7 +17,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.0.0 +HAP-python==3.1.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -481,7 +481,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.7 +denonavr==0.9.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 @@ -553,7 +553,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.2.4 +env_canada==0.2.5 # homeassistant.components.envirophat # envirophat==0.0.6 @@ -1298,7 +1298,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.nissan_leaf -pycarwings2==2.9 +pycarwings2==2.10 # homeassistant.components.cloudflare pycfdns==1.2.1 @@ -1337,7 +1337,7 @@ pydaikin==2.3.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==76 +pydeconz==77 # homeassistant.components.delijn pydelijn==0.6.1 @@ -1455,7 +1455,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.2.1 +pyiqvia==0.3.1 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 @@ -1542,7 +1542,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.11 +pymyq==2.0.12 # homeassistant.components.mysensors pymysensors==0.18.0 @@ -1858,7 +1858,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.4 +pytradfri[async]==7.0.5 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation @@ -1958,7 +1958,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.25 +roonapi==0.0.28 # homeassistant.components.rova rova==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8d3dfedfc8..bd9819319f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==3.0.0 +HAP-python==3.1.0 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -254,7 +254,7 @@ debugpy==1.2.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.7 +denonavr==0.9.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 @@ -673,7 +673,7 @@ pycountry==19.8.18 pydaikin==2.3.1 # homeassistant.components.deconz -pydeconz==76 +pydeconz==77 # homeassistant.components.dexcom pydexcom==0.2.0 @@ -734,7 +734,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.2.1 +pyiqvia==0.3.1 # homeassistant.components.isy994 pyisy==2.1.0 @@ -782,7 +782,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.11 +pymyq==2.0.12 # homeassistant.components.nut pynut2==2.1.2 @@ -915,7 +915,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.4 +pytradfri[async]==7.0.5 # homeassistant.components.vera pyvera==0.3.11 @@ -960,7 +960,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.25 +roonapi==0.0.28 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py new file mode 100644 index 00000000000..033b4aa7b4d --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -0,0 +1,79 @@ +""" +Test against characteristics captured from a Velux Gateway. + +https://github.com/home-assistant/core/issues/44314 +""" + +from homeassistant.components.cover import ( + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, +) + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_simpleconnect_cover_setup(hass): + """Test that a velux gateway can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "velux_gateway.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Check that the cover is correctly found and set up + cover_id = "cover.velux_window" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-1111111a114a111a-8" + + cover_helper = Helper( + hass, + cover_id, + pairing, + accessories[0], + config_entry, + ) + + cover_state = await cover_helper.poll_and_get_state() + assert cover_state.attributes["friendly_name"] == "VELUX Window" + assert cover_state.state == "closed" + assert cover_state.attributes["supported_features"] == ( + SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_OPEN + ) + + # Check that one of the sensors is correctly found and set up + sensor_id = "sensor.velux_sensor_temperature" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-a11b111-8" + + sensor_helper = Helper( + hass, + sensor_id, + pairing, + accessories[0], + config_entry, + ) + + sensor_state = await sensor_helper.poll_and_get_state() + assert sensor_state.attributes["friendly_name"] == "VELUX Sensor Temperature" + assert sensor_state.state == "18.9" + + # The cover and sensor are different devices (accessories) attached to the same bridge + assert cover.device_id != sensor.device_id + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(cover.device_id) + assert device.manufacturer == "VELUX" + assert device.name == "VELUX Window" + assert device.model == "VELUX Window" + assert device.sw_version == "48" + + bridge = device_registry.async_get(device.via_device_id) + assert bridge.manufacturer == "VELUX" + assert bridge.name == "VELUX Gateway" + assert bridge.model == "VELUX Gateway" + assert bridge.sw_version == "70" diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 42fc5dc7a49..2c88533f30d 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -21,7 +21,42 @@ from tests.common import ( from tests.components.blueprint.conftest import stub_blueprint_populate # noqa -async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): +async def test_get_triggers_btn(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): + """Test we get the expected triggers from a discovered mqtt device.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["btn"][0] = 1 + config["btn"][1] = 1 + config["so"]["13"] = 1 + config["so"]["73"] = 1 + mac = config["mac"] + + async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_1_SINGLE", + "type": "button_short_press", + "subtype": "button_1", + }, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_2_SINGLE", + "type": "button_short_press", + "subtype": "button_2", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_swc(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): """Test we get the expected triggers from a discovered mqtt device.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 0 @@ -239,13 +274,83 @@ async def test_update_remove_triggers( assert triggers == [] -async def test_if_fires_on_mqtt_message( +async def test_if_fires_on_mqtt_message_btn( hass, device_reg, calls, mqtt_mock, setup_tasmota ): - """Test triggers firing.""" + """Test button triggers firing.""" + # Discover a device with 2 device triggers + config = copy.deepcopy(DEFAULT_CONFIG) + config["btn"][0] = 1 + config["btn"][2] = 1 + config["so"]["73"] = 1 + mac = config["mac"] + + async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_1_SINGLE", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_1")}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_3_SINGLE", + "subtype": "button_3", + "type": "button_short_press", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_3")}, + }, + }, + ] + }, + ) + + # Fake button 1 single press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Button1":{"Action":"SINGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "short_press_1" + + # Fake button 3 single press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Button3":{"Action":"SINGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "short_press_3" + + +async def test_if_fires_on_mqtt_message_swc( + hass, device_reg, calls, mqtt_mock, setup_tasmota +): + """Test switch triggers firing.""" # Discover a device with 2 device triggers config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 0 + config["swc"][1] = 0 config["swc"][2] = 9 config["swn"][2] = "custom_switch" mac = config["mac"] @@ -270,7 +375,21 @@ async def test_if_fires_on_mqtt_message( }, "action": { "service": "test.automation", - "data_template": {"some": ("short_press")}, + "data_template": {"some": ("short_press_1")}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_switch_2_TOGGLE", + "type": "button_short_press", + "subtype": "switch_2", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_2")}, }, }, { @@ -284,28 +403,36 @@ async def test_if_fires_on_mqtt_message( }, "action": { "service": "test.automation", - "data_template": {"some": ("long_press")}, + "data_template": {"some": ("long_press_3")}, }, }, ] }, ) - # Fake short press. + # Fake switch 1 short press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() assert len(calls) == 1 - assert calls[0].data["some"] == "short_press" + assert calls[0].data["some"] == "short_press_1" - # Fake long press. + # Fake switch 2 short press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Switch2":{"Action":"TOGGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "short_press_2" + + # Fake switch 3 long press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"custom_switch":{"Action":"HOLD"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "long_press" + assert len(calls) == 3 + assert calls[2].data["some"] == "long_press_3" async def test_if_fires_on_mqtt_message_late_discover( diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 828cf1fb7b4..822a274bf23 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -11,6 +11,7 @@ from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -626,6 +627,7 @@ async def test_if_fires_on_change_with_for_0_advanced(hass, calls): async def test_if_fires_on_change_with_for_2(hass, calls): """Test for firing on change with for.""" + context = Context() assert await async_setup_component( hass, automation.DOMAIN, @@ -636,17 +638,33 @@ async def test_if_fires_on_change_with_for_2(hass, calls): "value_template": "{{ is_state('test.entity', 'world') }}", "for": 5, }, - "action": {"service": "test.automation"}, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, } }, ) - hass.states.async_set("test.entity", "world") + hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() assert len(calls) == 1 + assert calls[0].context.parent_id == context.id + assert calls[0].data["some"] == "template - test.entity - hello - world - 0:00:05" async def test_if_not_fires_on_change_with_for(hass, calls): @@ -811,3 +829,58 @@ async def test_invalid_for_template_1(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() assert mock_logger.error.called + + +async def test_if_fires_on_time_change(hass, calls): + """Test for firing on time changes.""" + start_time = dt_util.utcnow() + timedelta(hours=24) + time_that_will_not_match_right_away = start_time.replace(minute=1, second=0) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "template", + "value_template": "{{ utcnow().minute % 2 == 0 }}", + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + assert len(calls) == 0 + + # Trigger once (match template) + first_time = start_time.replace(minute=2, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=first_time): + async_fire_time_changed(hass, first_time) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (match template) + second_time = start_time.replace(minute=4, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=second_time): + async_fire_time_changed(hass, second_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (do not match template) + third_time = start_time.replace(minute=5, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=third_time): + async_fire_time_changed(hass, third_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (match template) + forth_time = start_time.replace(minute=8, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=forth_time): + async_fire_time_changed(hass, forth_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 2 diff --git a/tests/fixtures/homekit_controller/velux_gateway.json b/tests/fixtures/homekit_controller/velux_gateway.json new file mode 100644 index 00000000000..1a6f60537b3 --- /dev/null +++ b/tests/fixtures/homekit_controller/velux_gateway.json @@ -0,0 +1,380 @@ +[ + { + "aid": 1, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Gateway" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Gateway" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "a1a11a1" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pr" + ], + "format": "string", + "value": "70" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "1.1.0" + } + ], + "hidden": false, + "primary": false + } + ] + }, + { + "aid": 2, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Sensor" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Sensor" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "a11b111" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pr" + ], + "format": "string", + "value": "16" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "0000008A-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "Temperature sensor" + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 18.9, + "minValue": 0, + "maxValue": 50, + "minStep": 0.1, + "unit": "celsius" + } + ], + "hidden": false, + "primary": true + }, + { + "type": "00000082-0000-1000-8000-0026BB765291", + "iid": 11, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": [ + "pr" + ], + "format": "string", + "value": "Humidity sensor" + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 58, + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "00000097-0000-1000-8000-0026BB765291", + "iid": 14, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 15, + "perms": [ + "pr" + ], + "format": "string", + "value": "Carbon Dioxide sensor" + }, + { + "type": "00000092-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 1, + "minValue": 0, + "minStep": 1 + }, + { + "type": "00000093-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 400, + "minValue": 0, + "maxValue": 5000 + } + ], + "hidden": false, + "primary": false + } + ] + }, + { + "aid": 3, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Window" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Window" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "1111111a114a111a" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pr" + ], + "format": "string", + "value": "48" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "0000008B-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "Roof Window" + }, + { + "type": "0000007C-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 100, + "minValue": 0, + "unit": "percentage", + "minStep": 1 + }, + { + "type": "0000006D-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 100, + "minValue": 0, + "unit": "percentage", + "minStep": 1 + }, + { + "type": "00000072-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 2, + "maxValue": 2, + "minValue": 0, + "minStep": 1 + } + ], + "hidden": false, + "primary": true + } + ] + } +] \ No newline at end of file