Skip to content

Commit

Permalink
Merge pull request #592 from contagon/webhook_trigger
Browse files Browse the repository at this point in the history
Webhook Trigger
  • Loading branch information
craigbarratt authored Jun 19, 2024
2 parents 8302c13 + 35252d1 commit 6eab8c2
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 12 deletions.
5 changes: 3 additions & 2 deletions custom_components/pyscript/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
EVENT_STATE_CHANGED,
SERVICE_RELOAD,
)
from homeassistant.core import Config, HomeAssistant, ServiceCall
from homeassistant.core import Config, Event as HAEvent, HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.core import Event as HAEvent
from homeassistant.helpers.restore_state import DATA_RESTORE_STATE
from homeassistant.loader import bind_hass

Expand All @@ -51,6 +50,7 @@
from .requirements import install_requirements
from .state import State, StateVal
from .trigger import TrigTime
from .webhook import Webhook

_LOGGER = logging.getLogger(LOGGER_PATH)

Expand Down Expand Up @@ -241,6 +241,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
Mqtt.init(hass)
TrigTime.init(hass)
State.init(hass)
Webhook.init(hass)
State.register_functions()
GlobalContextMgr.init()

Expand Down
10 changes: 5 additions & 5 deletions custom_components/pyscript/entity.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""Entity Classes"""
"""Entity Classes."""
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import StateType
from homeassistant.const import STATE_UNKNOWN


class PyscriptEntity(RestoreEntity):
"""Generic Pyscript Entity"""
"""Generic Pyscript Entity."""

_attr_extra_state_attributes: dict
_attr_state: StateType = STATE_UNKNOWN

def set_state(self, state):
"""Set the state"""
"""Set the state."""
self._attr_state = state

def set_attributes(self, attributes):
"""Set Attributes"""
"""Set Attributes."""
self._attr_extra_state_attributes = attributes
20 changes: 20 additions & 0 deletions custom_components/pyscript/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"state_trigger",
"event_trigger",
"mqtt_trigger",
"webhook_trigger",
"state_active",
"time_active",
"task_unique",
Expand All @@ -74,6 +75,14 @@
"trigger_time",
"var_name",
"value",
"webhook_id",
}

WEBHOOK_METHODS = {
"GET",
"HEAD",
"POST",
"PUT",
}


Expand Down Expand Up @@ -363,6 +372,7 @@ async def trigger_init(self, trig_ctx, func_name):
"mqtt_trigger",
"state_trigger",
"time_trigger",
"webhook_trigger",
}
arg_check = {
"event_trigger": {"arg_cnt": {1, 2, 3}, "rep_ok": True},
Expand All @@ -373,6 +383,7 @@ async def trigger_init(self, trig_ctx, func_name):
"task_unique": {"arg_cnt": {1, 2}},
"time_active": {"arg_cnt": {"*"}},
"time_trigger": {"arg_cnt": {0, "*"}, "rep_ok": True},
"webhook_trigger": {"arg_cnt": {1, 2}, "rep_ok": True},
}
kwarg_check = {
"event_trigger": {"kwargs": {dict}},
Expand All @@ -388,6 +399,11 @@ async def trigger_init(self, trig_ctx, func_name):
"state_hold_false": {int, float},
"watch": {set, list},
},
"webhook_trigger": {
"kwargs": {dict},
"local_only": {bool},
"methods": {list, set},
},
}

for dec in self.decorators:
Expand Down Expand Up @@ -517,6 +533,10 @@ async def do_service_call(func, ast_ctx, data):
self.trigger_service.add(srv_name)
continue

if dec_name == "webhook_trigger" and "methods" in dec_kwargs:
if len(bad := set(dec_kwargs["methods"]).difference(WEBHOOK_METHODS)) > 0:
raise TypeError(f"{exc_mesg}: {bad} aren't valid {dec_name} methods")

if dec_name not in trig_decs:
trig_decs[dec_name] = []
if len(trig_decs[dec_name]) > 0 and "rep_ok" not in arg_info:
Expand Down
87 changes: 85 additions & 2 deletions custom_components/pyscript/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .function import Function
from .mqtt import Mqtt
from .state import STATE_VIRTUAL_ATTRS, State
from .webhook import Webhook

_LOGGER = logging.getLogger(LOGGER_PATH + ".trigger")

Expand Down Expand Up @@ -222,13 +223,22 @@ async def wait_until(
time_trigger=None,
event_trigger=None,
mqtt_trigger=None,
webhook_trigger=None,
webhook_local_only=True,
webhook_methods=None,
timeout=None,
state_hold=None,
state_hold_false=None,
__test_handshake__=None,
):
"""Wait for zero or more triggers, until an optional timeout."""
if state_trigger is None and time_trigger is None and event_trigger is None and mqtt_trigger is None:
if (
state_trigger is None
and time_trigger is None
and event_trigger is None
and mqtt_trigger is None
and webhook_trigger is None
):
if timeout is not None:
await asyncio.sleep(timeout)
return {"trigger_type": "timeout"}
Expand All @@ -238,6 +248,7 @@ async def wait_until(
state_trig_eval = None
event_trig_expr = None
mqtt_trig_expr = None
webhook_trig_expr = None
exc = None
notify_q = asyncio.Queue(0)

Expand Down Expand Up @@ -349,6 +360,26 @@ async def wait_until(
State.notify_del(state_trig_ident, notify_q)
raise exc
await Mqtt.notify_add(mqtt_trigger[0], notify_q)
if webhook_trigger is not None:
if isinstance(webhook_trigger, str):
webhook_trigger = [webhook_trigger]
if len(webhook_trigger) > 1:
webhook_trig_expr = AstEval(
f"{ast_ctx.name} webhook_trigger",
ast_ctx.get_global_ctx(),
logger_name=ast_ctx.get_logger_name(),
)
Function.install_ast_funcs(webhook_trig_expr)
webhook_trig_expr.parse(webhook_trigger[1], mode="eval")
exc = webhook_trig_expr.get_exception_obj()
if exc is not None:
if len(state_trig_ident) > 0:
State.notify_del(state_trig_ident, notify_q)
raise exc
if webhook_methods is None:
webhook_methods = {"POST", "PUT"}
Webhook.notify_add(webhook_trigger[0], webhook_local_only, webhook_methods, notify_q)

time0 = time.monotonic()

if __test_handshake__:
Expand Down Expand Up @@ -394,7 +425,12 @@ async def wait_until(
state_trig_timeout = True
time_next = now + dt.timedelta(seconds=this_timeout)
if this_timeout is None:
if state_trigger is None and event_trigger is None and mqtt_trigger is None:
if (
state_trigger is None
and event_trigger is None
and mqtt_trigger is None
and webhook_trigger is None
):
_LOGGER.debug(
"trigger %s wait_until no next time - returning with none",
ast_ctx.name,
Expand Down Expand Up @@ -527,6 +563,17 @@ async def wait_until(
if mqtt_trig_ok:
ret = notify_info
break
elif notify_type == "webhook":
if webhook_trig_expr is None:
ret = notify_info
break
webhook_trig_ok = await webhook_trig_expr.eval(notify_info)
exc = webhook_trig_expr.get_exception_obj()
if exc is not None:
break
if webhook_trig_ok:
ret = notify_info
break
else:
_LOGGER.error(
"trigger %s wait_until got unexpected queue message %s",
Expand All @@ -540,6 +587,8 @@ async def wait_until(
Event.notify_del(event_trigger[0], notify_q)
if mqtt_trigger is not None:
Mqtt.notify_del(mqtt_trigger[0], notify_q)
if webhook_trigger is not None:
Webhook.notify_del(webhook_trigger[0], notify_q)
if exc:
raise exc
return ret
Expand Down Expand Up @@ -826,6 +875,10 @@ def __init__(
self.event_trigger_kwargs = trig_cfg.get("event_trigger", {}).get("kwargs", {})
self.mqtt_trigger = trig_cfg.get("mqtt_trigger", {}).get("args", None)
self.mqtt_trigger_kwargs = trig_cfg.get("mqtt_trigger", {}).get("kwargs", {})
self.webhook_trigger = trig_cfg.get("webhook_trigger", {}).get("args", None)
self.webhook_trigger_kwargs = trig_cfg.get("webhook_trigger", {}).get("kwargs", {})
self.webhook_local_only = self.webhook_trigger_kwargs.get("local_only", True)
self.webhook_methods = self.webhook_trigger_kwargs.get("methods", {"POST", "PUT"})
self.state_active = trig_cfg.get("state_active", {}).get("args", None)
self.time_active = trig_cfg.get("time_active", {}).get("args", None)
self.time_active_hold_off = trig_cfg.get("time_active", {}).get("kwargs", {}).get("hold_off", None)
Expand All @@ -842,6 +895,7 @@ def __init__(
self.state_trig_ident_any = set()
self.event_trig_expr = None
self.mqtt_trig_expr = None
self.webhook_trig_expr = None
self.have_trigger = False
self.setup_ok = False
self.run_on_startup = False
Expand Down Expand Up @@ -933,6 +987,21 @@ def __init__(
return
self.have_trigger = True

if self.webhook_trigger is not None:
if len(self.webhook_trigger) == 2:
self.webhook_trig_expr = AstEval(
f"{self.name} @webhook_trigger()",
self.global_ctx,
logger_name=self.name,
)
Function.install_ast_funcs(self.webhook_trig_expr)
self.webhook_trig_expr.parse(self.webhook_trigger[1], mode="eval")
exc = self.webhook_trig_expr.get_exception_long()
if exc is not None:
self.webhook_trig_expr.get_logger().error(exc)
return
self.have_trigger = True

self.setup_ok = True

def stop(self):
Expand All @@ -945,6 +1014,8 @@ def stop(self):
Event.notify_del(self.event_trigger[0], self.notify_q)
if self.mqtt_trigger is not None:
Mqtt.notify_del(self.mqtt_trigger[0], self.notify_q)
if self.webhook_trigger is not None:
Webhook.notify_del(self.webhook_trigger[0], self.notify_q)
if self.task:
Function.reaper_cancel(self.task)
self.task = None
Expand Down Expand Up @@ -995,6 +1066,11 @@ async def trigger_watch(self):
if self.mqtt_trigger is not None:
_LOGGER.debug("trigger %s adding mqtt_trigger %s", self.name, self.mqtt_trigger[0])
await Mqtt.notify_add(self.mqtt_trigger[0], self.notify_q)
if self.webhook_trigger is not None:
_LOGGER.debug("trigger %s adding webhook_trigger %s", self.name, self.webhook_trigger[0])
Webhook.notify_add(
self.webhook_trigger[0], self.webhook_local_only, self.webhook_methods, self.notify_q
)

last_trig_time = None
last_state_trig_time = None
Expand Down Expand Up @@ -1182,6 +1258,11 @@ async def trigger_watch(self):
user_kwargs = self.mqtt_trigger_kwargs.get("kwargs", {})
if self.mqtt_trig_expr:
trig_ok = await self.mqtt_trig_expr.eval(notify_info)
elif notify_type == "webhook":
func_args = notify_info
user_kwargs = self.webhook_trigger_kwargs.get("kwargs", {})
if self.webhook_trig_expr:
trig_ok = await self.webhook_trig_expr.eval(notify_info)

else:
user_kwargs = self.time_trigger_kwargs.get("kwargs", {})
Expand Down Expand Up @@ -1237,6 +1318,8 @@ async def trigger_watch(self):
Event.notify_del(self.event_trigger[0], self.notify_q)
if self.mqtt_trigger is not None:
Mqtt.notify_del(self.mqtt_trigger[0], self.notify_q)
if self.webhook_trigger is not None:
Webhook.notify_del(self.webhook_trigger[0], self.notify_q)
return

def call_action(self, notify_type, func_args, run_task=True):
Expand Down
Loading

0 comments on commit 6eab8c2

Please sign in to comment.