Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webhook Trigger #592

Merged
merged 9 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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