Skip to content

Commit 99ea648

Browse files
committed
Added webhook trigger - mimics event trigger for now
1 parent 9ba1f43 commit 99ea648

File tree

4 files changed

+143
-2
lines changed

4 files changed

+143
-2
lines changed

custom_components/pyscript/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from .requirements import install_requirements
5252
from .state import State, StateVal
5353
from .trigger import TrigTime
54+
from .webhook import Webhook
5455

5556
_LOGGER = logging.getLogger(LOGGER_PATH)
5657

@@ -241,6 +242,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
241242
Mqtt.init(hass)
242243
TrigTime.init(hass)
243244
State.init(hass)
245+
Webhook.init(hass)
244246
State.register_functions()
245247
GlobalContextMgr.init()
246248

custom_components/pyscript/eval.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"state_trigger",
5151
"event_trigger",
5252
"mqtt_trigger",
53+
"webhook_trigger",
5354
"state_active",
5455
"time_active",
5556
"task_unique",
@@ -363,6 +364,7 @@ async def trigger_init(self, trig_ctx, func_name):
363364
"mqtt_trigger",
364365
"state_trigger",
365366
"time_trigger",
367+
"webhook_trigger",
366368
}
367369
arg_check = {
368370
"event_trigger": {"arg_cnt": {1, 2, 3}, "rep_ok": True},
@@ -373,6 +375,8 @@ async def trigger_init(self, trig_ctx, func_name):
373375
"task_unique": {"arg_cnt": {1, 2}},
374376
"time_active": {"arg_cnt": {"*"}},
375377
"time_trigger": {"arg_cnt": {0, "*"}, "rep_ok": True},
378+
# TODO: Add in functionality to webhook with arguments
379+
"webhook_trigger": {"arg_cnt": {1, 2, 3}, "rep_ok": True},
376380
}
377381
kwarg_check = {
378382
"event_trigger": {"kwargs": {dict}},
@@ -388,6 +392,8 @@ async def trigger_init(self, trig_ctx, func_name):
388392
"state_hold_false": {int, float},
389393
"watch": {set, list},
390394
},
395+
# "webhook_trigger": {"call": {str, list}, "local": bool},
396+
"webhook_trigger": {"kwargs": {dict}},
391397
}
392398

393399
for dec in self.decorators:

custom_components/pyscript/trigger.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .function import Function
2222
from .mqtt import Mqtt
2323
from .state import STATE_VIRTUAL_ATTRS, State
24+
from .webhook import Webhook
2425

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

@@ -222,13 +223,14 @@ async def wait_until(
222223
time_trigger=None,
223224
event_trigger=None,
224225
mqtt_trigger=None,
226+
webhook_trigger=None,
225227
timeout=None,
226228
state_hold=None,
227229
state_hold_false=None,
228230
__test_handshake__=None,
229231
):
230232
"""Wait for zero or more triggers, until an optional timeout."""
231-
if state_trigger is None and time_trigger is None and event_trigger is None and mqtt_trigger is None:
233+
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:
232234
if timeout is not None:
233235
await asyncio.sleep(timeout)
234236
return {"trigger_type": "timeout"}
@@ -238,6 +240,7 @@ async def wait_until(
238240
state_trig_eval = None
239241
event_trig_expr = None
240242
mqtt_trig_expr = None
243+
webhook_trig_expr = None
241244
exc = None
242245
notify_q = asyncio.Queue(0)
243246

@@ -349,6 +352,22 @@ async def wait_until(
349352
State.notify_del(state_trig_ident, notify_q)
350353
raise exc
351354
await Mqtt.notify_add(mqtt_trigger[0], notify_q)
355+
if webhook_trigger is not None:
356+
if isinstance(webhook_trigger, str):
357+
webhook_trigger = [webhook_trigger]
358+
if len(webhook_trigger) > 1:
359+
webhook_trig_expr = AstEval(
360+
f"{ast_ctx.name} webhook_trigger",
361+
ast_ctx.get_global_ctx(),
362+
logger_name=ast_ctx.get_logger_name(),
363+
)
364+
Function.install_ast_funcs(webhook_trig_expr)
365+
webhook_trig_expr.parse(webhook_trigger[1], mode="eval")
366+
exc = webhook_trig_expr.get_exception_obj()
367+
if exc is not None:
368+
if len(state_trig_ident) > 0:
369+
State.notify_del(state_trig_ident, notify_q)
370+
raise exc
352371
time0 = time.monotonic()
353372

354373
if __test_handshake__:
@@ -394,7 +413,7 @@ async def wait_until(
394413
state_trig_timeout = True
395414
time_next = now + dt.timedelta(seconds=this_timeout)
396415
if this_timeout is None:
397-
if state_trigger is None and event_trigger is None and mqtt_trigger is None:
416+
if state_trigger is None and event_trigger is None and mqtt_trigger is None and webhook_trigger is None:
398417
_LOGGER.debug(
399418
"trigger %s wait_until no next time - returning with none",
400419
ast_ctx.name,
@@ -527,6 +546,17 @@ async def wait_until(
527546
if mqtt_trig_ok:
528547
ret = notify_info
529548
break
549+
elif notify_type == "webhook":
550+
if webhook_trig_expr is None:
551+
ret = notify_info
552+
break
553+
webhook_trig_ok = await webhook_trig_expr.eval(notify_info)
554+
exc = webhook_trig_expr.get_exception_obj()
555+
if exc is not None:
556+
break
557+
if webhook_trig_ok:
558+
ret = notify_info
559+
break
530560
else:
531561
_LOGGER.error(
532562
"trigger %s wait_until got unexpected queue message %s",
@@ -540,6 +570,8 @@ async def wait_until(
540570
Event.notify_del(event_trigger[0], notify_q)
541571
if mqtt_trigger is not None:
542572
Mqtt.notify_del(mqtt_trigger[0], notify_q)
573+
if webhook_trigger is not None:
574+
Webhook.notify_del(webhook_trigger[0], notify_q)
543575
if exc:
544576
raise exc
545577
return ret
@@ -826,6 +858,8 @@ def __init__(
826858
self.event_trigger_kwargs = trig_cfg.get("event_trigger", {}).get("kwargs", {})
827859
self.mqtt_trigger = trig_cfg.get("mqtt_trigger", {}).get("args", None)
828860
self.mqtt_trigger_kwargs = trig_cfg.get("mqtt_trigger", {}).get("kwargs", {})
861+
self.webhook_trigger = trig_cfg.get("webhook_trigger", {}).get("args", None)
862+
self.webhook_trigger_kwargs = trig_cfg.get("webhook_trigger", {}).get("kwargs", {})
829863
self.state_active = trig_cfg.get("state_active", {}).get("args", None)
830864
self.time_active = trig_cfg.get("time_active", {}).get("args", None)
831865
self.time_active_hold_off = trig_cfg.get("time_active", {}).get("kwargs", {}).get("hold_off", None)
@@ -842,6 +876,7 @@ def __init__(
842876
self.state_trig_ident_any = set()
843877
self.event_trig_expr = None
844878
self.mqtt_trig_expr = None
879+
self.webhook_trig_expr = None
845880
self.have_trigger = False
846881
self.setup_ok = False
847882
self.run_on_startup = False
@@ -933,6 +968,21 @@ def __init__(
933968
return
934969
self.have_trigger = True
935970

971+
if self.webhook_trigger is not None:
972+
if len(self.webhook_trigger) == 2:
973+
self.webhook_trig_expr = AstEval(
974+
f"{self.name} @webhook_trigger()",
975+
self.global_ctx,
976+
logger_name=self.name,
977+
)
978+
Function.install_ast_funcs(self.webhook_trig_expr)
979+
self.webhook_trig_expr.parse(self.webhook_trigger[1], mode="eval")
980+
exc = self.webhook_trig_expr.get_exception_long()
981+
if exc is not None:
982+
self.webhook_trig_expr.get_logger().error(exc)
983+
return
984+
self.have_trigger = True
985+
936986
self.setup_ok = True
937987

938988
def stop(self):
@@ -945,6 +995,8 @@ def stop(self):
945995
Event.notify_del(self.event_trigger[0], self.notify_q)
946996
if self.mqtt_trigger is not None:
947997
Mqtt.notify_del(self.mqtt_trigger[0], self.notify_q)
998+
if self.webhook_trigger is not None:
999+
Webhook.notify_del(self.webhook_trigger[0], self.notify_q)
9481000
if self.task:
9491001
Function.reaper_cancel(self.task)
9501002
self.task = None
@@ -995,6 +1047,9 @@ async def trigger_watch(self):
9951047
if self.mqtt_trigger is not None:
9961048
_LOGGER.debug("trigger %s adding mqtt_trigger %s", self.name, self.mqtt_trigger[0])
9971049
await Mqtt.notify_add(self.mqtt_trigger[0], self.notify_q)
1050+
if self.webhook_trigger is not None:
1051+
_LOGGER.debug("trigger %s adding webhook_trigger %s", self.name, self.webhook_trigger[0])
1052+
Webhook.notify_add(self.webhook_trigger[0], self.notify_q)
9981053

9991054
last_trig_time = None
10001055
last_state_trig_time = None
@@ -1237,6 +1292,8 @@ async def trigger_watch(self):
12371292
Event.notify_del(self.event_trigger[0], self.notify_q)
12381293
if self.mqtt_trigger is not None:
12391294
Mqtt.notify_del(self.mqtt_trigger[0], self.notify_q)
1295+
if self.webhook_trigger is not None:
1296+
Webhook.notify_del(self.webhook_trigger[0], self.notify_q)
12401297
return
12411298

12421299
def call_action(self, notify_type, func_args, run_task=True):

custom_components/pyscript/webhook.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Handles webhooks and notification."""
2+
3+
import logging
4+
5+
from .const import LOGGER_PATH
6+
7+
_LOGGER = logging.getLogger(LOGGER_PATH + ".webhook")
8+
9+
10+
class Webhook:
11+
"""Define webhook functions."""
12+
13+
#
14+
# Global hass instance
15+
#
16+
hass = None
17+
18+
#
19+
# notify message queues by webhook type
20+
#
21+
notify = {}
22+
notify_remove = {}
23+
24+
def __init__(self):
25+
"""Warn on Webhook instantiation."""
26+
_LOGGER.error("Webhook class is not meant to be instantiated")
27+
28+
@classmethod
29+
def init(cls, hass):
30+
"""Initialize Webhook."""
31+
32+
cls.hass = hass
33+
34+
@classmethod
35+
async def webhook_listener(cls, event):
36+
"""Listen callback for given webhook which updates any notifications."""
37+
38+
func_args = {
39+
"trigger_type": "event",
40+
"event_type": event.event_type,
41+
"context": event.context,
42+
}
43+
func_args.update(event.data)
44+
await cls.update(event.event_type, func_args)
45+
46+
@classmethod
47+
def notify_add(cls, webhook_type, queue):
48+
"""Register to notify for webhooks of given type to be sent to queue."""
49+
50+
if webhook_type not in cls.notify:
51+
cls.notify[webhook_type] = set()
52+
_LOGGER.debug("webhook.notify_add(%s) -> adding webhook listener", webhook_type)
53+
cls.notify_remove[webhook_type] = cls.hass.bus.async_listen(webhook_type, cls.webhook_listener)
54+
cls.notify[webhook_type].add(queue)
55+
56+
@classmethod
57+
def notify_del(cls, webhook_type, queue):
58+
"""Unregister to notify for webhooks of given type for given queue."""
59+
60+
if webhook_type not in cls.notify or queue not in cls.notify[webhook_type]:
61+
return
62+
cls.notify[webhook_type].discard(queue)
63+
if len(cls.notify[webhook_type]) == 0:
64+
cls.notify_remove[webhook_type]()
65+
_LOGGER.debug("webhook.notify_del(%s) -> removing webhook listener", webhook_type)
66+
del cls.notify[webhook_type]
67+
del cls.notify_remove[webhook_type]
68+
69+
@classmethod
70+
async def update(cls, webhook_type, func_args):
71+
"""Deliver all notifications for an webhook of the given type."""
72+
73+
_LOGGER.debug("webhook.update(%s, %s)", webhook_type, func_args)
74+
if webhook_type in cls.notify:
75+
for queue in cls.notify[webhook_type]:
76+
await queue.put(["webhook", func_args.copy()])

0 commit comments

Comments
 (0)