def state_change_listener(event): """Handle specific state changes.""" # Skip if the event's source does not match the trigger's source. from_state = event.data.get("old_state") to_state = event.data.get("new_state") if not source_match(from_state, source) and not source_match( to_state, source): return zone_state = hass.states.get(zone_entity_id) from_match = (condition.zone(hass, zone_state, from_state) if from_state else False) to_match = condition.zone(hass, zone_state, to_state) if to_state else False if (trigger_event == EVENT_ENTER and not from_match and to_match or trigger_event == EVENT_LEAVE and from_match and not to_match): hass.async_run_hass_job( job, { "trigger": { "platform": "geo_location", "source": source, "entity_id": event.data.get("entity_id"), "from_state": from_state, "to_state": to_state, "zone": zone_state, "event": trigger_event, "description": f"geo_location - {source}", } }, event.context, )
def zone_automation_listener(zone_event): """Listen for state changes and calls action.""" entity = zone_event.data.get("entity_id") from_s = zone_event.data.get("old_state") to_s = zone_event.data.get("new_state") if (from_s and not location.has_location(from_s) or not location.has_location(to_s)): return zone_state = hass.states.get(zone_entity_id) from_match = condition.zone(hass, zone_state, from_s) if from_s else False to_match = condition.zone(hass, zone_state, to_s) if to_s else False if (event == EVENT_ENTER and not from_match and to_match or event == EVENT_LEAVE and from_match and not to_match): description = f"{entity} {_EVENT_DESCRIPTION[event]} {zone_state.attributes[ATTR_FRIENDLY_NAME]}" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "zone": zone_state, "event": event, "description": description, } }, to_s.context, )
def zone_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" if from_s and not location.has_location(from_s) or \ not location.has_location(to_s): return zone_state = hass.states.get(zone_entity_id) if from_s: from_match = condition.zone(hass, zone_state, from_s) else: from_match = False to_match = condition.zone(hass, zone_state, to_s) # pylint: disable=too-many-boolean-expressions if event == EVENT_ENTER and not from_match and to_match or \ event == EVENT_LEAVE and from_match and not to_match: hass.async_add_job(action, { 'trigger': { 'platform': 'zone', 'entity_id': entity, 'from_state': from_s, 'to_state': to_s, 'zone': zone_state, 'event': event, }, })
def zone_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" if from_s and not location.has_location(from_s) or not location.has_location(to_s): return zone_state = hass.states.get(zone_entity_id) if from_s: from_match = condition.zone(hass, zone_state, from_s) else: from_match = False to_match = condition.zone(hass, zone_state, to_s) # pylint: disable=too-many-boolean-expressions if event == EVENT_ENTER and not from_match and to_match or event == EVENT_LEAVE and from_match and not to_match: action( { "trigger": { "platform": "zone", "entity_id": entity, "from_state": from_s, "to_state": to_s, "zone": zone_state, "event": event, } } )
def zone_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" if from_s and not location.has_location(from_s) or \ not location.has_location(to_s): return zone_state = hass.states.get(zone_entity_id) if from_s: from_match = condition.zone(hass, zone_state, from_s) else: from_match = False to_match = condition.zone(hass, zone_state, to_s) # pylint: disable=too-many-boolean-expressions if event == EVENT_ENTER and not from_match and to_match or \ event == EVENT_LEAVE and from_match and not to_match: hass.async_run_job( action, { 'trigger': { 'platform': 'zone', 'entity_id': entity, 'from_state': from_s, 'to_state': to_s, 'zone': zone_state, 'event': event, }, })
async def async_attach_trigger( hass, config, action, automation_info, *, platform_type: str = "zone" ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) event = config.get(CONF_EVENT) job = HassJob(action) @callback def zone_automation_listener(zone_event): """Listen for state changes and calls action.""" entity = zone_event.data.get("entity_id") from_s = zone_event.data.get("old_state") to_s = zone_event.data.get("new_state") if ( from_s and not location.has_location(from_s) or not location.has_location(to_s) ): return if not (zone_state := hass.states.get(zone_entity_id)): _LOGGER.warning( "Automation '%s' is referencing non-existing zone '%s' in a zone trigger", automation_info["name"], zone_entity_id, ) return from_match = condition.zone(hass, zone_state, from_s) if from_s else False to_match = condition.zone(hass, zone_state, to_s) if to_s else False if ( event == EVENT_ENTER and not from_match and to_match or event == EVENT_LEAVE and from_match and not to_match ): description = f"{entity} {_EVENT_DESCRIPTION[event]} {zone_state.attributes[ATTR_FRIENDLY_NAME]}" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "zone": zone_state, "event": event, "description": description, } }, to_s.context, )
def state_change_listener(event): """Handle specific state changes.""" # Skip if the event is not a geo_location entity. if not event.data.get("entity_id").startswith(DOMAIN): return # Skip if the event's source does not match the trigger's source. from_state = event.data.get("old_state") to_state = event.data.get("new_state") if not source_match(from_state, source) and not source_match( to_state, source): return zone_state = hass.states.get(zone_entity_id) from_match = condition.zone(hass, zone_state, from_state) to_match = condition.zone(hass, zone_state, to_state) # pylint: disable=too-many-boolean-expressions if (trigger_event == EVENT_ENTER and not from_match and to_match or trigger_event == EVENT_LEAVE and from_match and not to_match): hass.async_run_job( action( { "trigger": { "platform": "geo_location", "source": source, "entity_id": event.data.get("entity_id"), "from_state": from_state, "to_state": to_state, "zone": zone_state, "event": trigger_event, } }, context=event.context, ))
def state_change_listener(event): """Handle specific state changes.""" # Skip if the event is not a geo_location entity. if not event.data.get('entity_id').startswith(DOMAIN): return # Skip if the event's source does not match the trigger's source. from_state = event.data.get('old_state') to_state = event.data.get('new_state') if not source_match(from_state, source) \ and not source_match(to_state, source): return zone_state = hass.states.get(zone_entity_id) from_match = condition.zone(hass, zone_state, from_state) to_match = condition.zone(hass, zone_state, to_state) # pylint: disable=too-many-boolean-expressions if trigger_event == EVENT_ENTER and not from_match and to_match or \ trigger_event == EVENT_LEAVE and from_match and not to_match: hass.async_run_job( action( { 'trigger': { 'platform': 'geo_location', 'source': source, 'entity_id': event.data.get('entity_id'), 'from_state': from_state, 'to_state': to_state, 'zone': zone_state, 'event': trigger_event, }, }, context=event.context))
def zone_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" if (from_s and not location.has_location(from_s) or not location.has_location(to_s)): return zone_state = hass.states.get(zone_entity_id) if from_s: from_match = condition.zone(hass, zone_state, from_s) else: from_match = False to_match = condition.zone(hass, zone_state, to_s) # pylint: disable=too-many-boolean-expressions if (event == EVENT_ENTER and not from_match and to_match or event == EVENT_LEAVE and from_match and not to_match): hass.async_run_job( action( { "trigger": { "platform": "zone", "entity_id": entity, "from_state": from_s, "to_state": to_s, "zone": zone_state, "event": event, } }, context=to_s.context, ))
def state_change_listener(event): """Handle specific state changes.""" # Skip if the event is not a geo_location entity. if not event.data.get('entity_id').startswith(DOMAIN): return # Skip if the event's source does not match the trigger's source. from_state = event.data.get('old_state') to_state = event.data.get('new_state') if not source_match(from_state, source) \ and not source_match(to_state, source): return zone_state = hass.states.get(zone_entity_id) from_match = condition.zone(hass, zone_state, from_state) to_match = condition.zone(hass, zone_state, to_state) # pylint: disable=too-many-boolean-expressions if trigger_event == EVENT_ENTER and not from_match and to_match or \ trigger_event == EVENT_LEAVE and from_match and not to_match: hass.async_run_job(action({ 'trigger': { 'platform': 'geo_location', 'source': source, 'entity_id': event.data.get('entity_id'), 'from_state': from_state, 'to_state': to_state, 'zone': zone_state, 'event': trigger_event, }, }, context=event.context))
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: AutomationActionType, automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] source: str = config[CONF_SOURCE].lower() zone_entity_id = config.get(CONF_ZONE) trigger_event = config.get(CONF_EVENT) job = HassJob(action) @callback def state_change_listener(event): """Handle specific state changes.""" # Skip if the event's source does not match the trigger's source. from_state = event.data.get("old_state") to_state = event.data.get("new_state") if not source_match(from_state, source) and not source_match( to_state, source): return if (zone_state := hass.states.get(zone_entity_id)) is None: _LOGGER.warning( "Unable to execute automation %s: Zone %s not found", automation_info["name"], zone_entity_id, ) return from_match = (condition.zone(hass, zone_state, from_state) if from_state else False) to_match = condition.zone(hass, zone_state, to_state) if to_state else False if (trigger_event == EVENT_ENTER and not from_match and to_match or trigger_event == EVENT_LEAVE and from_match and not to_match): hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "geo_location", "source": source, "entity_id": event.data.get("entity_id"), "from_state": from_state, "to_state": to_state, "zone": zone_state, "event": trigger_event, "description": f"geo_location - {source}", } }, event.context, )
def zone_automation_listener(zone_event): """Listen for state changes and calls action.""" entity = zone_event.data.get("entity_id") from_s = zone_event.data.get("old_state") to_s = zone_event.data.get("new_state") if ( from_s and not location.has_location(from_s) or not location.has_location(to_s) ): return zone_state = hass.states.get(zone_entity_id) from_match = condition.zone(hass, zone_state, from_s) if from_s else False to_match = condition.zone(hass, zone_state, to_s) if ( event == EVENT_ENTER and not from_match and to_match or event == EVENT_LEAVE and from_match and not to_match ): hass.async_run_job( action( { "trigger": { "platform": "zone", "entity_id": entity, "from_state": from_s, "to_state": to_s, "zone": zone_state, "event": event, } }, context=to_s.context, ) )
async def test_zone_raises(hass): """Test that zone raises ConditionError on errors.""" test = await condition.async_from_config( hass, { "condition": "zone", "entity_id": "device_tracker.cat", "zone": "zone.home", }, ) with pytest.raises(ConditionError, match="no zone"): condition.zone(hass, zone_ent=None, entity="sensor.any") with pytest.raises(ConditionError, match="unknown zone"): test(hass) hass.states.async_set( "zone.home", "zoning", {"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10}, ) with pytest.raises(ConditionError, match="no entity"): condition.zone(hass, zone_ent="zone.home", entity=None) with pytest.raises(ConditionError, match="unknown entity"): test(hass) hass.states.async_set( "device_tracker.cat", "home", {"friendly_name": "cat"}, ) with pytest.raises(ConditionError, match="latitude"): test(hass) hass.states.async_set( "device_tracker.cat", "home", {"friendly_name": "cat", "latitude": 2.1}, ) with pytest.raises(ConditionError, match="longitude"): test(hass) hass.states.async_set( "device_tracker.cat", "home", {"friendly_name": "cat", "latitude": 2.1, "longitude": 1.1}, ) # All okay, now test multiple failed conditions assert test(hass) test = await condition.async_from_config( hass, { "condition": "zone", "entity_id": ["device_tracker.cat", "device_tracker.dog"], "zone": ["zone.home", "zone.work"], }, ) with pytest.raises(ConditionError, match="dog"): test(hass) with pytest.raises(ConditionError, match="work"): test(hass) hass.states.async_set( "zone.work", "zoning", {"name": "work", "latitude": 20, "longitude": 10, "radius": 25000}, ) hass.states.async_set( "device_tracker.dog", "work", {"friendly_name": "dog", "latitude": 20.1, "longitude": 10.1}, ) assert test(hass)