moat-kv-cal 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moat_kv_cal-0.1.2/LICENSE +3 -0
- moat_kv_cal-0.1.2/LICENSE.txt +14 -0
- moat_kv_cal-0.1.2/PKG-INFO +31 -0
- moat_kv_cal-0.1.2/README.rst +10 -0
- moat_kv_cal-0.1.2/moat/kv/cal/__init__.py +3 -0
- moat_kv_cal-0.1.2/moat/kv/cal/_cfg.yaml +2 -0
- moat_kv_cal-0.1.2/moat/kv/cal/_main.py +266 -0
- moat_kv_cal-0.1.2/moat/kv/cal/model.py +94 -0
- moat_kv_cal-0.1.2/moat/kv/cal/util.py +115 -0
- moat_kv_cal-0.1.2/moat_kv_cal.egg-info/PKG-INFO +31 -0
- moat_kv_cal-0.1.2/moat_kv_cal.egg-info/SOURCES.txt +14 -0
- moat_kv_cal-0.1.2/moat_kv_cal.egg-info/dependency_links.txt +1 -0
- moat_kv_cal-0.1.2/moat_kv_cal.egg-info/requires.txt +3 -0
- moat_kv_cal-0.1.2/moat_kv_cal.egg-info/top_level.txt +1 -0
- moat_kv_cal-0.1.2/pyproject.toml +97 -0
- moat_kv_cal-0.1.2/setup.cfg +4 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
The code in this repository, and all MoaT submodules it refers to,
|
2
|
+
is part of the MoaT project.
|
3
|
+
|
4
|
+
Unless a submodule's LICENSE.txt states otherwise, all included files are
|
5
|
+
licensed under the LGPL V3, as published by the FSF at
|
6
|
+
https://www.gnu.org/licenses/lgpl-3.0.html .
|
7
|
+
|
8
|
+
In addition to the LGPL's terms, the author(s) respectfully ask all users of
|
9
|
+
this code to contribute any bug fixes or enhancements. Also, please link back to
|
10
|
+
https://M-o-a-T.org.
|
11
|
+
|
12
|
+
Thank you.
|
13
|
+
|
14
|
+
Copyright © 2021 ff.: the MoaT contributor(s), as per the git changelog(s).
|
@@ -0,0 +1,31 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: moat-kv-cal
|
3
|
+
Version: 0.1.2
|
4
|
+
Summary: Calendar management for MoaT-KV
|
5
|
+
Author-email: Matthias Urlichs <matthias@urlichs.de>
|
6
|
+
Project-URL: homepage, https://m-o-a-t.org
|
7
|
+
Project-URL: repository, https://github.com/M-o-a-T/moat
|
8
|
+
Keywords: MoaT
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Classifier: Topic :: Database
|
13
|
+
Classifier: Topic :: Home Automation
|
14
|
+
Requires-Python: >=3.8
|
15
|
+
Description-Content-Type: text/x-rst
|
16
|
+
License-File: LICENSE.txt
|
17
|
+
Requires-Dist: moat-kv~=0.70.23
|
18
|
+
Requires-Dist: moat-util~=0.56.4
|
19
|
+
Requires-Dist: aiocaldav
|
20
|
+
Dynamic: license-file
|
21
|
+
|
22
|
+
===========
|
23
|
+
MoaT-KV-Cal
|
24
|
+
===========
|
25
|
+
|
26
|
+
MoaT-KV-Cal is a calendar extension to MoaT-KV.
|
27
|
+
|
28
|
+
It can monitor a calendar (or specific entries thereof), publish pending
|
29
|
+
entries, set alerts, control that they are acknowledged, and escalate
|
30
|
+
if/when they are not.
|
31
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
===========
|
2
|
+
MoaT-KV-Cal
|
3
|
+
===========
|
4
|
+
|
5
|
+
MoaT-KV-Cal is a calendar extension to MoaT-KV.
|
6
|
+
|
7
|
+
It can monitor a calendar (or specific entries thereof), publish pending
|
8
|
+
entries, set alerts, control that they are acknowledged, and escalate
|
9
|
+
if/when they are not.
|
10
|
+
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# command line interface
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import asyncclick as click
|
5
|
+
from functools import partial
|
6
|
+
from moat.util import P, Path
|
7
|
+
from moat.kv.data import data_get
|
8
|
+
from .model import CalRoot
|
9
|
+
from .util import find_next_alarm
|
10
|
+
from datetime import datetime, timezone, timedelta
|
11
|
+
import pytz
|
12
|
+
|
13
|
+
import anyio
|
14
|
+
import aiocaldav as caldav
|
15
|
+
|
16
|
+
import logging
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
utc = timezone.utc
|
21
|
+
now = partial(datetime.now, utc)
|
22
|
+
|
23
|
+
|
24
|
+
@click.group(short_help="Manage calendar polling.")
|
25
|
+
@click.pass_obj
|
26
|
+
async def cli(obj):
|
27
|
+
"""
|
28
|
+
List known calendars and poll them.
|
29
|
+
"""
|
30
|
+
obj.data = await CalRoot.as_handler(obj.client)
|
31
|
+
|
32
|
+
|
33
|
+
@cli.command("run")
|
34
|
+
@click.pass_obj
|
35
|
+
async def run_(obj):
|
36
|
+
"""Process calendar alarms"""
|
37
|
+
from moat.kv.client import client_scope
|
38
|
+
|
39
|
+
kv = await client_scope(**obj.cfg.kv)
|
40
|
+
cal_cfg = (await kv.get(P("calendar.test"))).value
|
41
|
+
try:
|
42
|
+
tz = pytz.timezone(cal_cfg["zone"])
|
43
|
+
except KeyError:
|
44
|
+
tz = utc
|
45
|
+
else:
|
46
|
+
now = partial(datetime.now, tz)
|
47
|
+
|
48
|
+
try:
|
49
|
+
t_scan = datetime.fromtimestamp(cal_cfg["scan"], utc)
|
50
|
+
except KeyError:
|
51
|
+
t_scan = now()
|
52
|
+
interval = timedelta(0, cal_cfg.get("interval", 1800))
|
53
|
+
|
54
|
+
try:
|
55
|
+
t_al = await kv.get(cal_cfg["dst"])
|
56
|
+
except KeyError:
|
57
|
+
t_al = now()
|
58
|
+
else:
|
59
|
+
t_al = datetime.fromtimestamp(t_al.value["time"], tz)
|
60
|
+
|
61
|
+
async with caldav.DAVClient(
|
62
|
+
url=cal_cfg["url"],
|
63
|
+
username=cal_cfg["user"],
|
64
|
+
password=cal_cfg["pass"],
|
65
|
+
) as client:
|
66
|
+
principal = await client.principal()
|
67
|
+
calendar = await principal.calendar(name="privat neu")
|
68
|
+
while True:
|
69
|
+
t_now = now()
|
70
|
+
if t_now < t_scan:
|
71
|
+
await anyio.sleep((t_scan - t_now).total_seconds())
|
72
|
+
cal_cfg = (await kv.get(P("calendar.test"))).value
|
73
|
+
cal_cfg["scan"] = t_scan.timestamp()
|
74
|
+
await kv.set(P("calendar.test"), value=cal_cfg)
|
75
|
+
t_now = t_scan
|
76
|
+
|
77
|
+
logger.info("Scan %s", t_scan)
|
78
|
+
ev, v, ev_t = await find_next_alarm(calendar, zone=tz, now=t_scan)
|
79
|
+
t_scan += interval
|
80
|
+
t_scan = max(t_now, t_scan).astimezone(tz)
|
81
|
+
|
82
|
+
if ev is None:
|
83
|
+
logger.warning("NO EVT")
|
84
|
+
continue
|
85
|
+
if evt <= t_now:
|
86
|
+
if t_al != ev_t:
|
87
|
+
# set alarm message
|
88
|
+
logger.warning("ALARM %s %s", v.summary.value, ev_t)
|
89
|
+
await kv.set(
|
90
|
+
cal_cfg["dst"],
|
91
|
+
value=dict(time=int(ev_t.timestamp()), info=v.summary.value),
|
92
|
+
)
|
93
|
+
t_al = ev_t
|
94
|
+
t_scan = t_now + timedelta(0, cal_cfg.get("interval", 1800) / 3)
|
95
|
+
elif ev_t < t_scan:
|
96
|
+
t_scan = ev_t
|
97
|
+
logger.warning("ScanEarly %s", t_scan)
|
98
|
+
else:
|
99
|
+
logger.warning("ScanLate %s", t_scan)
|
100
|
+
|
101
|
+
|
102
|
+
@cli.command("list")
|
103
|
+
@click.pass_obj
|
104
|
+
async def list_(obj):
|
105
|
+
"""Emit the current state as a YAML file."""
|
106
|
+
prefix = obj.cfg.kv.cal.prefix
|
107
|
+
path = Path()
|
108
|
+
|
109
|
+
def pm(p):
|
110
|
+
if len(p) == 0:
|
111
|
+
return p
|
112
|
+
elif not isinstance(p[0], int):
|
113
|
+
return None
|
114
|
+
elif len(p) == 1:
|
115
|
+
return Path("%02x" % p[0])
|
116
|
+
else:
|
117
|
+
return Path("%02x.%12x" % (p[0], p[1])) + p[2:]
|
118
|
+
|
119
|
+
if obj.meta:
|
120
|
+
|
121
|
+
def pm(p):
|
122
|
+
return Path(str(prefix + path)) + p
|
123
|
+
|
124
|
+
await data_get(obj, prefix + path, as_dict="_", path_mangle=pm)
|
125
|
+
|
126
|
+
|
127
|
+
# @cli.command("attr", help="Mirror a device attribute to/from MoaT-KV")
|
128
|
+
# @click.option("-d", "--device", help="Device to access.")
|
129
|
+
# @click.option("-f", "--family", help="Device family to modify.")
|
130
|
+
# @click.option("-i", "--interval", type=float, help="read value every N seconds")
|
131
|
+
# @click.option("-w", "--write", is_flag=True, help="Write to the device")
|
132
|
+
# @click.option("-a", "--attr", "attr_", help="The node's attribute to use", default=":")
|
133
|
+
# @click.argument("attr", nargs=1)
|
134
|
+
# @click.argument("path", nargs=1)
|
135
|
+
# @click.pass_obj
|
136
|
+
# async def attr__(obj, device, family, write, attr, interval, path, attr_):
|
137
|
+
# """Show/add/modify an entry to repeatedly read an 1wire device's attribute.
|
138
|
+
#
|
139
|
+
# You can only set an interval, not a path, on family codes.
|
140
|
+
# A path of '-' deletes the entry.
|
141
|
+
# If you set neither interval nor path, reports the current
|
142
|
+
# values.
|
143
|
+
# """
|
144
|
+
# path = P(path)
|
145
|
+
# prefix = obj.cfg.kv.cal.prefix
|
146
|
+
# if (device is not None) + (family is not None) != 1:
|
147
|
+
# raise click.UsageError("Either family or device code must be given")
|
148
|
+
# if interval and write:
|
149
|
+
# raise click.UsageError("Writing isn't polled")
|
150
|
+
#
|
151
|
+
# remove = False
|
152
|
+
# if len(path) == 1 and path[0] == "-":
|
153
|
+
# path = ()
|
154
|
+
# remove = True
|
155
|
+
#
|
156
|
+
# if family:
|
157
|
+
# if path:
|
158
|
+
# raise click.UsageError("You cannot set a per-family path")
|
159
|
+
# fd = (int(family, 16),)
|
160
|
+
# else:
|
161
|
+
# f, d = device.split(".", 2)[0:2]
|
162
|
+
# fd = (int(f, 16), int(d, 16))
|
163
|
+
#
|
164
|
+
# attr = P(attr)
|
165
|
+
# attr_ = P(attr_)
|
166
|
+
# if remove:
|
167
|
+
# res = await obj.client.delete(prefix + fd + attr)
|
168
|
+
# else:
|
169
|
+
# val = dict()
|
170
|
+
# if path:
|
171
|
+
# val["src" if write else "dest"] = path
|
172
|
+
# if interval:
|
173
|
+
# val["interval"] = interval
|
174
|
+
# if len(attr_):
|
175
|
+
# val["src_attr" if write else "dest_attr"] = attr_
|
176
|
+
#
|
177
|
+
# res = await obj.client.set(prefix + fd + attr, value=val)
|
178
|
+
#
|
179
|
+
# if res is not None and obj.meta:
|
180
|
+
# yprint(res, stream=obj.stdout)
|
181
|
+
#
|
182
|
+
#
|
183
|
+
# @cli.command("set")
|
184
|
+
# @click.option("-d", "--device", help="Device to modify.")
|
185
|
+
# @click.option("-f", "--family", help="Device family to modify.")
|
186
|
+
# @attr_args
|
187
|
+
# @click.argument("subpath", nargs=1, type=P, default=P(":"))
|
188
|
+
# @click.pass_obj
|
189
|
+
# async def set_(obj, device, family, subpath, **kw):
|
190
|
+
# """Set or delete some random attribute.
|
191
|
+
#
|
192
|
+
# For deletion, use '-e ATTR -'.
|
193
|
+
# """
|
194
|
+
# if (device is not None) + (family is not None) != 1:
|
195
|
+
# raise click.UsageError("Either family or device code must be given")
|
196
|
+
#
|
197
|
+
# if family:
|
198
|
+
# fd = (int(family, 16),)
|
199
|
+
# if len(subpath):
|
200
|
+
# raise click.UsageError("You can't use a subpath here.")
|
201
|
+
# else:
|
202
|
+
# f, d = device.split(".", 2)[0:2]
|
203
|
+
# fd = (int(f, 16), int(d, 16))
|
204
|
+
#
|
205
|
+
# res = await node_attr(obj, obj.cfg.kv.cal.prefix + fd + subpath, **kw)
|
206
|
+
# if res and obj.meta:
|
207
|
+
# yprint(res, stream=obj.stdout)
|
208
|
+
#
|
209
|
+
#
|
210
|
+
# @cli.command("server")
|
211
|
+
# @click.option("-h", "--host", help="Host name of this server.")
|
212
|
+
# @click.option("-p", "--port", help="Port of this server.")
|
213
|
+
# @click.option("-d", "--delete", is_flag=True, help="Delete this server.")
|
214
|
+
# @click.argument("name", nargs=-1)
|
215
|
+
# @click.pass_obj
|
216
|
+
# async def server_(obj, name, host, port, delete):
|
217
|
+
# """
|
218
|
+
# Configure a server.
|
219
|
+
#
|
220
|
+
# No arguments: list them.
|
221
|
+
# """
|
222
|
+
# prefix = obj.cfg.kv.cal.prefix
|
223
|
+
# if not name:
|
224
|
+
# if host or port or delete:
|
225
|
+
# raise click.UsageError("Use a server name to set parameters")
|
226
|
+
# async for r in obj.client.get_tree(
|
227
|
+
# prefix | "server", min_depth=1, max_depth=1
|
228
|
+
# ):
|
229
|
+
# print(r.path[-1], file=obj.stdout)
|
230
|
+
# return
|
231
|
+
# elif len(name) > 1:
|
232
|
+
# raise click.UsageError("Only one server allowed")
|
233
|
+
# name = name[0]
|
234
|
+
# if host or port:
|
235
|
+
# if delete:
|
236
|
+
# raise click.UsageError("You can't delete and set at the same time.")
|
237
|
+
# value = attrdict()
|
238
|
+
# if host:
|
239
|
+
# value.host = host
|
240
|
+
# if port:
|
241
|
+
# if port == "-":
|
242
|
+
# value.port = NotGiven
|
243
|
+
# else:
|
244
|
+
# value.port = int(port)
|
245
|
+
# elif delete:
|
246
|
+
# res = await obj.client.delete_tree(prefix | "server" | name, nchain=obj.meta)
|
247
|
+
# if obj.meta:
|
248
|
+
# yprint(res, stream=obj.stdout)
|
249
|
+
# return
|
250
|
+
# else:
|
251
|
+
# value = None
|
252
|
+
# res = await node_attr(
|
253
|
+
# obj, prefix | "server" | name, ((P("server"), value),),(),())
|
254
|
+
# if res and obj.meta:
|
255
|
+
# yprint(res, stream=obj.stdout)
|
256
|
+
#
|
257
|
+
#
|
258
|
+
# @cli.command()
|
259
|
+
# @click.pass_obj
|
260
|
+
# @click.argument("server", nargs=-1)
|
261
|
+
# async def monitor(obj, server):
|
262
|
+
# """Stand-alone task to monitor one or more OWFS servers."""
|
263
|
+
# from .task import task
|
264
|
+
#
|
265
|
+
# async with as_service(obj) as srv:
|
266
|
+
# await task(obj.client, obj.cfg, server, srv)
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Moat-KV client data model for calendars
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
|
8
|
+
from moat.kv.obj import ClientEntry, ClientRoot, AttrClientEntry
|
9
|
+
from moat.kv.errors import ErrorRoot
|
10
|
+
|
11
|
+
import logging
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class CalAlarm(AttrClientEntry):
|
17
|
+
"""
|
18
|
+
An alarm handler. ``True`` is sent to @cmd. If @state doesn't follow
|
19
|
+
within @timeout (default ten) seconds, raise an alarm and skip the
|
20
|
+
delay. Otherwise go to the next entry.
|
21
|
+
|
22
|
+
@src if set overrides the acknowledge source of the calendar.
|
23
|
+
"""
|
24
|
+
|
25
|
+
ATTRS = ("cmd", "state", "delay", "src", "timeout")
|
26
|
+
|
27
|
+
cls = ClientEntry
|
28
|
+
|
29
|
+
|
30
|
+
class CalEntry(AttrClientEntry):
|
31
|
+
"""
|
32
|
+
A calendar entry to be monitored specifically (usually recurring).
|
33
|
+
|
34
|
+
The entry's name is the UUID in the parent calendar.
|
35
|
+
|
36
|
+
Summary+start+duration are updated from the calendar when it changes.
|
37
|
+
@alarms, if set, modify when the entry's alarm sequence should trigger.
|
38
|
+
@src if set overrides the acknowledge source of the calendar.
|
39
|
+
"""
|
40
|
+
|
41
|
+
ATTRS = ("summary", "start", "duration", "alarms", "src")
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def child_type(cls, name):
|
45
|
+
if isinstance(name, int):
|
46
|
+
return CalAlarm
|
47
|
+
return ClientEntry
|
48
|
+
|
49
|
+
|
50
|
+
class CalBase(AttrClientEntry):
|
51
|
+
"""
|
52
|
+
A monitored calendar. Every @freq seconds the CalDAV server at @url
|
53
|
+
is queried for events during the next @days. They are published
|
54
|
+
to @dst/UUID as records with summary/start/duration/alarmtime(s)/UID.
|
55
|
+
|
56
|
+
@dst gets the same data, but for the next-most alarm time.
|
57
|
+
@src is the signal that the alarm has been acknowledged.
|
58
|
+
"""
|
59
|
+
|
60
|
+
ATTRS = ("url", "username", "password", "freq", "days", "dst", "src")
|
61
|
+
|
62
|
+
@classmethod
|
63
|
+
def child_type(cls, name):
|
64
|
+
if isinstance(name, int):
|
65
|
+
return CalAlarm
|
66
|
+
return CalEntry
|
67
|
+
|
68
|
+
|
69
|
+
class CalRoot(ClientRoot):
|
70
|
+
cls = {}
|
71
|
+
reg = {}
|
72
|
+
CFG = "cal"
|
73
|
+
err = None
|
74
|
+
|
75
|
+
async def run_starting(self):
|
76
|
+
if self.err is None:
|
77
|
+
self.err = await ErrorRoot.as_handler(self.client)
|
78
|
+
await super().run_starting()
|
79
|
+
|
80
|
+
@property
|
81
|
+
def server(self):
|
82
|
+
return self["server"]
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def register(cls, typ):
|
86
|
+
def acc(kls):
|
87
|
+
cls.reg[typ] = kls
|
88
|
+
return kls
|
89
|
+
|
90
|
+
return acc
|
91
|
+
|
92
|
+
@classmethod
|
93
|
+
def child_type(kls, name):
|
94
|
+
return CalBase
|
@@ -0,0 +1,115 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from datetime import datetime, timedelta, timezone, date, time
|
4
|
+
from dateutil.rrule import rrulestr
|
5
|
+
from vobject.icalendar import VAlarm, VEvent
|
6
|
+
|
7
|
+
import logging
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
async def find_next_alarm(calendar, future=10, now=None, zone=timezone.utc) -> Tuple(
|
13
|
+
VAlarm,
|
14
|
+
datetime,
|
15
|
+
):
|
16
|
+
"""
|
17
|
+
fetch the next alarm in the current calendar
|
18
|
+
|
19
|
+
returns an (event, alarm_time) tuple
|
20
|
+
"""
|
21
|
+
## It should theoretically be possible to find both the events and
|
22
|
+
## tasks in one calendar query, but not all server implementations
|
23
|
+
## supports it, hence either event, todo or journal should be set
|
24
|
+
## to True when searching. Here is a date search for events, with
|
25
|
+
## expand:
|
26
|
+
events_fetched = await calendar.search(
|
27
|
+
start=datetime.now(),
|
28
|
+
end=datetime.now() + timedelta(days=future),
|
29
|
+
event=True,
|
30
|
+
expand=False,
|
31
|
+
)
|
32
|
+
|
33
|
+
if now is None:
|
34
|
+
now = datetime.now(timezone.utc)
|
35
|
+
ev = None
|
36
|
+
ev_v = None
|
37
|
+
ev_t = None
|
38
|
+
|
39
|
+
for e in events_fetched:
|
40
|
+
vx = None
|
41
|
+
vx_t = None
|
42
|
+
rids = set()
|
43
|
+
# create a list of superseded events
|
44
|
+
for v in e.vobject_instance.components():
|
45
|
+
if v.behavior is not VEvent:
|
46
|
+
continue
|
47
|
+
try:
|
48
|
+
rid = v.recurrence_id
|
49
|
+
except AttributeError:
|
50
|
+
continue
|
51
|
+
else:
|
52
|
+
rids.add(rid.value)
|
53
|
+
|
54
|
+
# find earliest event, skipping superseded ones
|
55
|
+
for v in e.vobject_instance.components():
|
56
|
+
if v.behavior is not VEvent:
|
57
|
+
continue
|
58
|
+
try:
|
59
|
+
rid = v.recurrence_id
|
60
|
+
except AttributeError:
|
61
|
+
rid = None
|
62
|
+
|
63
|
+
t_start = next_start(v, now)
|
64
|
+
if t_start is None:
|
65
|
+
raise ValueError("Start time: ??")
|
66
|
+
# t_start = next_start(v, now)
|
67
|
+
if not t_start.tzinfo:
|
68
|
+
t_start = t_start.astimezone(zone)
|
69
|
+
if rid is None and t_start in rids:
|
70
|
+
continue
|
71
|
+
|
72
|
+
if vx is None or t_start < vx_t:
|
73
|
+
vx, vx_t = v, t_start
|
74
|
+
|
75
|
+
for al in vx.components():
|
76
|
+
if al.behavior is not VAlarm:
|
77
|
+
continue
|
78
|
+
if not al.useBegin:
|
79
|
+
continue
|
80
|
+
if isinstance(t_start, date) and not isinstance(t_start, datetime):
|
81
|
+
t_start = datetime.combine(t_start, time(0), tzinfo=zone)
|
82
|
+
# XXX TODO count back from the current timezone's midnight
|
83
|
+
|
84
|
+
t_al = t_start + al.trigger.value
|
85
|
+
if t_al < now:
|
86
|
+
continue
|
87
|
+
if ev is None or ev_t > t_al:
|
88
|
+
ev, ev_v, ev_t = e, vx, t_al
|
89
|
+
if ev:
|
90
|
+
logger.warning("Next alarm: %s at %s", ev.vobject_instance.vevent.summary.value, ev_t)
|
91
|
+
return ev, ev_v, ev_t
|
92
|
+
|
93
|
+
|
94
|
+
def next_start(v, now):
|
95
|
+
st = v.dtstart.value
|
96
|
+
try:
|
97
|
+
rule = rrulestr(v.rrule.value, dtstart=st)
|
98
|
+
except AttributeError:
|
99
|
+
pass
|
100
|
+
else:
|
101
|
+
excl = set()
|
102
|
+
for edt in v.contents.get("exdate", ()):
|
103
|
+
for ed in edt.value:
|
104
|
+
excl.add(ed)
|
105
|
+
|
106
|
+
st = rule.after(now, inc=True)
|
107
|
+
while st in excl:
|
108
|
+
st = rule.after(edt, inc=False)
|
109
|
+
|
110
|
+
for edt in v.contents.get("rdate", ()):
|
111
|
+
for ed in edt.value:
|
112
|
+
if now <= ed.value < st:
|
113
|
+
st = ed.value
|
114
|
+
|
115
|
+
return st
|
@@ -0,0 +1,31 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: moat-kv-cal
|
3
|
+
Version: 0.1.2
|
4
|
+
Summary: Calendar management for MoaT-KV
|
5
|
+
Author-email: Matthias Urlichs <matthias@urlichs.de>
|
6
|
+
Project-URL: homepage, https://m-o-a-t.org
|
7
|
+
Project-URL: repository, https://github.com/M-o-a-T/moat
|
8
|
+
Keywords: MoaT
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Classifier: Topic :: Database
|
13
|
+
Classifier: Topic :: Home Automation
|
14
|
+
Requires-Python: >=3.8
|
15
|
+
Description-Content-Type: text/x-rst
|
16
|
+
License-File: LICENSE.txt
|
17
|
+
Requires-Dist: moat-kv~=0.70.23
|
18
|
+
Requires-Dist: moat-util~=0.56.4
|
19
|
+
Requires-Dist: aiocaldav
|
20
|
+
Dynamic: license-file
|
21
|
+
|
22
|
+
===========
|
23
|
+
MoaT-KV-Cal
|
24
|
+
===========
|
25
|
+
|
26
|
+
MoaT-KV-Cal is a calendar extension to MoaT-KV.
|
27
|
+
|
28
|
+
It can monitor a calendar (or specific entries thereof), publish pending
|
29
|
+
entries, set alerts, control that they are acknowledged, and escalate
|
30
|
+
if/when they are not.
|
31
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
LICENSE
|
2
|
+
LICENSE.txt
|
3
|
+
README.rst
|
4
|
+
pyproject.toml
|
5
|
+
moat/kv/cal/__init__.py
|
6
|
+
moat/kv/cal/_cfg.yaml
|
7
|
+
moat/kv/cal/_main.py
|
8
|
+
moat/kv/cal/model.py
|
9
|
+
moat/kv/cal/util.py
|
10
|
+
moat_kv_cal.egg-info/PKG-INFO
|
11
|
+
moat_kv_cal.egg-info/SOURCES.txt
|
12
|
+
moat_kv_cal.egg-info/dependency_links.txt
|
13
|
+
moat_kv_cal.egg-info/requires.txt
|
14
|
+
moat_kv_cal.egg-info/top_level.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
moat
|
@@ -0,0 +1,97 @@
|
|
1
|
+
[build-system]
|
2
|
+
build-backend = "setuptools.build_meta"
|
3
|
+
requires = [ "setuptools", "wheel",]
|
4
|
+
|
5
|
+
[project]
|
6
|
+
classifiers = [
|
7
|
+
"Development Status :: 4 - Beta",
|
8
|
+
"Intended Audience :: Information Technology",
|
9
|
+
"Programming Language :: Python :: 3",
|
10
|
+
"Topic :: Database",
|
11
|
+
"Topic :: Home Automation",
|
12
|
+
]
|
13
|
+
dependencies = [
|
14
|
+
"moat-kv ~= 0.70.23",
|
15
|
+
"moat-util ~= 0.56.4",
|
16
|
+
"aiocaldav",
|
17
|
+
]
|
18
|
+
version = "0.1.2"
|
19
|
+
keywords = [ "MoaT",]
|
20
|
+
requires-python = ">=3.8"
|
21
|
+
name = "moat-kv-cal"
|
22
|
+
description = "Calendar management for MoaT-KV"
|
23
|
+
readme = "README.rst"
|
24
|
+
license-files = ["LICENSE.txt"]
|
25
|
+
|
26
|
+
[[project.authors]]
|
27
|
+
email = "matthias@urlichs.de"
|
28
|
+
name = "Matthias Urlichs"
|
29
|
+
|
30
|
+
[project.urls]
|
31
|
+
homepage = "https://m-o-a-t.org"
|
32
|
+
repository = "https://github.com/M-o-a-T/moat"
|
33
|
+
|
34
|
+
[tool.flake8]
|
35
|
+
max-line-length = 99
|
36
|
+
ignore = [ "F841", "F401", "E731", "E502", "E402", "E127", "E123", "W503", "E231", "E203", "E501" ]
|
37
|
+
|
38
|
+
[tool.isort]
|
39
|
+
line_length = 99
|
40
|
+
multi_line_output = 3
|
41
|
+
profile = "black"
|
42
|
+
|
43
|
+
[tool.setuptools]
|
44
|
+
packages = [ "moat.kv.cal",]
|
45
|
+
[tool.setuptools.package-data]
|
46
|
+
"*" = ["*.yaml"]
|
47
|
+
|
48
|
+
[tool.setuptools_scm]
|
49
|
+
|
50
|
+
[tool.black]
|
51
|
+
line-length = 99
|
52
|
+
|
53
|
+
[tool.tox]
|
54
|
+
legacy_tox_ini = """
|
55
|
+
[tox]
|
56
|
+
isolated_build = True
|
57
|
+
envlist = py310,check
|
58
|
+
|
59
|
+
[testenv]
|
60
|
+
setenv =
|
61
|
+
PYTHONPATH = {env:PYTHONPATH}{:}{toxinidir}
|
62
|
+
deps =
|
63
|
+
anyio
|
64
|
+
asyncwebsockets
|
65
|
+
asyncclick
|
66
|
+
asyncscope
|
67
|
+
trio
|
68
|
+
pytest
|
69
|
+
commands =
|
70
|
+
python3 -mpytest tests/
|
71
|
+
|
72
|
+
[testenv:check]
|
73
|
+
commands =
|
74
|
+
pylint moat tests
|
75
|
+
flake8p moat tests
|
76
|
+
black --check moat tests
|
77
|
+
deps =
|
78
|
+
pytest
|
79
|
+
pylint
|
80
|
+
black
|
81
|
+
flake8-pyproject
|
82
|
+
flake8
|
83
|
+
|
84
|
+
"""
|
85
|
+
|
86
|
+
[tool.pytest]
|
87
|
+
filterwarnings = [
|
88
|
+
"error",
|
89
|
+
"ignore:unclosed:ResourceWarning",
|
90
|
+
]
|
91
|
+
addopts = "--verbose"
|
92
|
+
|
93
|
+
[tool.pylint]
|
94
|
+
[tool.pylint.messages_control]
|
95
|
+
disable = "wrong-import-order,ungrouped-imports,too-many-nested-blocks,use-dict-literal,unspecified-encoding,too-many-statements,too-many-return-statements,too-many-locals,too-many-instance-attributes,too-many-branches,too-many-arguments,too-few-public-methods,superfluous-parens,no-else-return,no-else-continue,invalid-name,fixme"
|
96
|
+
|
97
|
+
[tool.moat]
|