initial commit
This commit is contained in:
75
CalendarSync.py
Normal file
75
CalendarSync.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import caldav
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
import vobject
|
||||
|
||||
class CalendarSync(object):
|
||||
log = logging.getLogger("CalendarSync")
|
||||
|
||||
startTime = "06:00"
|
||||
|
||||
def __init__(self):
|
||||
self._calendar = None
|
||||
self._client = None
|
||||
|
||||
def connect(self, url, user, passwd):
|
||||
self._client = caldav.DAVClient(url=url, username=user, password=passwd)
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._client is not None
|
||||
|
||||
def getExistingMuellEvents(self) -> list[caldav.Event]:
|
||||
events = self._calendar.events()
|
||||
ret = []
|
||||
for e in events:
|
||||
try:
|
||||
uid = e.vobject_instance.vevent.uid.value
|
||||
m = re.match(r'(.+)@MyMuell', uid)
|
||||
if m is not None:
|
||||
ret.append(e)
|
||||
except AttributeError:
|
||||
continue
|
||||
return ret
|
||||
|
||||
def createEvent(self, uid, summary, date):
|
||||
date_start = datetime.datetime.combine(date, datetime.time.fromisoformat(CalendarSync.startTime))
|
||||
date_end = date_start + datetime.timedelta(minutes=5)
|
||||
|
||||
cal = vobject.iCalendar()
|
||||
ev = cal.add('vevent')
|
||||
ev.add("summary").value = summary
|
||||
ev.add("dtstart").value = date_start
|
||||
ev.add("dtend").value = date_end
|
||||
ev.add("uid").value = str(uid) + "@MyMuell"
|
||||
ev.add("valarm").add("trigger").value = datetime.timedelta(hours=-12)
|
||||
ev.add("valarm").add("trigger").value = datetime.timedelta(hours=0)
|
||||
|
||||
self._calendar.save_event(cal.serialize())
|
||||
|
||||
def syncEvents(self, e):
|
||||
self.createEvent(
|
||||
e["id"],
|
||||
e["title"],
|
||||
datetime.datetime.strptime(e["day"], "%Y-%m-%d"))
|
||||
|
||||
def getCalendars(self):
|
||||
principal = self._client.principal()
|
||||
cals = principal.calendars()
|
||||
ret = []
|
||||
for c in cals:
|
||||
ret.append(c.name)
|
||||
return ret
|
||||
|
||||
def createCalendar(self, cal):
|
||||
principal = self._client.principal()
|
||||
cals = principal.calendars()
|
||||
|
||||
for c in cals:
|
||||
if c.name == cal:
|
||||
self._calendar = c
|
||||
|
||||
if self._calendar is None:
|
||||
CalendarSync.log.info("creating new calendar \"{}\"".format(cal))
|
||||
self._calendar = principal.make_calendar(name=cal)
|
||||
35
GuiWorker.py
Normal file
35
GuiWorker.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
from PyQt5.QtWidgets import QProgressBar, QPushButton, QStatusBar
|
||||
|
||||
|
||||
class GuiWorker(QThread):
|
||||
finished = pyqtSignal(bool, str)
|
||||
rangeChanged = pyqtSignal(int, int)
|
||||
progressChanged = pyqtSignal(int)
|
||||
stateChanged = pyqtSignal(str)
|
||||
|
||||
_button = None
|
||||
|
||||
def __init__(self, runnable, parent=None):
|
||||
QThread.__init__(self, parent)
|
||||
self._callable = runnable
|
||||
|
||||
def connectProgressBar(self, progress_bar: QProgressBar):
|
||||
self.progressChanged.connect(progress_bar.setValue)
|
||||
self.rangeChanged.connect(progress_bar.setRange)
|
||||
progress_bar.setTextVisible(True)
|
||||
|
||||
|
||||
def connectButton(self, button: QPushButton):
|
||||
self._button = button
|
||||
self._button.clicked.connect(self.start)
|
||||
|
||||
def connectStatusBar(self, statusbar: QStatusBar):
|
||||
self.stateChanged.connect(lambda val: statusbar.showMessage(val))
|
||||
|
||||
def run(self):
|
||||
if self._button:
|
||||
self._button.setEnabled(False)
|
||||
ret = self._callable(self)
|
||||
self._button.setEnabled(True)
|
||||
self.finished.emit(ret[0], ret[1])
|
||||
75
LocalDataStorage.py
Normal file
75
LocalDataStorage.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from appdirs import *
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
class LocalDataStorage(object):
|
||||
def __init__(self):
|
||||
self.appname = "MyMuellDav"
|
||||
self.appauthor = "Av3m"
|
||||
self.__fernet = Fernet(b'kWUFurHmtMWX6nOMhpFR45DpuNVPckSQ9t95_ADG2dA=')
|
||||
|
||||
if not os.path.exists(self.user_data_dir):
|
||||
os.makedirs(self.user_data_dir)
|
||||
|
||||
DefaultSettings = {
|
||||
'url': '',
|
||||
'user': '',
|
||||
'password': '',
|
||||
'calendar': '',
|
||||
'mymuellcity': '',
|
||||
}
|
||||
|
||||
@property
|
||||
def user_data_dir(self):
|
||||
return user_data_dir(self.appname, self.appauthor)
|
||||
|
||||
@property
|
||||
def file_settings(self):
|
||||
return os.path.join(self.user_data_dir, "settings.json")
|
||||
@property
|
||||
def file_city_data(self):
|
||||
return os.path.join(self.user_data_dir, "city_data.json")
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
if os.path.exists(self.file_settings):
|
||||
with open(self.file_settings, "r") as f:
|
||||
j = json.load(f)
|
||||
j["password"] = str(self.__fernet.decrypt(bytes(j["password"], encoding="utf-8")), encoding="utf-8")
|
||||
return j
|
||||
else:
|
||||
return LocalDataStorage.DefaultSettings
|
||||
|
||||
@settings.setter
|
||||
def settings(self, val):
|
||||
if val is None and os.path.exists(self.file_settings):
|
||||
os.remove(self.file_settings)
|
||||
return
|
||||
|
||||
with open(self.file_settings, "w+") as f:
|
||||
v = copy.copy(val)
|
||||
v["password"] = str(self.__fernet.encrypt(bytes(v["password"], encoding="utf-8")), encoding="utf-8")
|
||||
json.dump(v, f)
|
||||
|
||||
os.chmod(self.file_settings, 0o0600)
|
||||
|
||||
@property
|
||||
def city_data(self):
|
||||
if os.path.exists(self.file_city_data):
|
||||
with open(self.file_city_data, "r") as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return None
|
||||
|
||||
@city_data.setter
|
||||
def city_data(self, val):
|
||||
if val is None and os.path.exists(self.file_city_data):
|
||||
os.remove(self.file_city_data)
|
||||
return
|
||||
|
||||
with open(self.file_city_data, "w+") as f:
|
||||
json.dump(val, f)
|
||||
|
||||
316
MyMuell2CalDavGui.py
Normal file
316
MyMuell2CalDavGui.py
Normal file
@@ -0,0 +1,316 @@
|
||||
from PyQt5.QtWidgets import \
|
||||
QApplication, \
|
||||
QWidget, \
|
||||
QListWidget, \
|
||||
QListWidgetItem, \
|
||||
QVBoxLayout, \
|
||||
QHBoxLayout, \
|
||||
QGridLayout, \
|
||||
QLineEdit, \
|
||||
QGroupBox, \
|
||||
QLabel, \
|
||||
QPushButton, \
|
||||
QMessageBox, \
|
||||
QComboBox, \
|
||||
QProgressBar, \
|
||||
QStatusBar, \
|
||||
QSizePolicy, \
|
||||
QMainWindow
|
||||
|
||||
|
||||
from PyQt5.QtCore import QModelIndex, Qt
|
||||
import MyMuellDataModel
|
||||
import sys
|
||||
import CalendarSync
|
||||
|
||||
from GuiWorker import GuiWorker
|
||||
|
||||
|
||||
class MyMuell2CalDavGui(QMainWindow):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._dataModel = MyMuellDataModel.MyMuellDataModel()
|
||||
self._davClient = CalendarSync.CalendarSync()
|
||||
|
||||
self._selectedCity = None
|
||||
|
||||
self._cities = []
|
||||
|
||||
self._citiesWidget = QListWidget()
|
||||
self._filterText = QLineEdit()
|
||||
|
||||
self._url = QLineEdit()
|
||||
self._user = QLineEdit()
|
||||
self._password = QLineEdit()
|
||||
self._calendarNames = QComboBox()
|
||||
self._connectButton = QPushButton("connect")
|
||||
self._syncButton = QPushButton("sync events")
|
||||
self._deleteButton = QPushButton("delete existing events")
|
||||
self._errorMessage = QMessageBox()
|
||||
self._progressBar = QProgressBar()
|
||||
self._statusBar = QStatusBar()
|
||||
|
||||
self._settings = self._dataModel.storage.settings
|
||||
|
||||
self._url.setText(self._settings["url"])
|
||||
self._password.setText(self._settings["password"])
|
||||
self._user.setText(self._settings["user"])
|
||||
|
||||
self._workerConnect = GuiWorker(self.runnable_connect_caldav)
|
||||
self._workerSync = GuiWorker(self.runnable_sync_events)
|
||||
self._workerDelete = GuiWorker(self.runnable_delete_events)
|
||||
|
||||
|
||||
self.initUI()
|
||||
|
||||
self.__fillCities()
|
||||
|
||||
def entrySelected(self, i: QListWidgetItem):
|
||||
city = self._dataModel.get_city_by_id(i.data(QListWidgetItem.UserType))
|
||||
|
||||
self._selectedCity = (city["id"], city["area_id"])
|
||||
|
||||
def saveSettings(self, val):
|
||||
self._settings["url"] = self._url.text()
|
||||
self._settings["user"] = self._user.text()
|
||||
self._settings["password"] = self._password.text()
|
||||
|
||||
if self._calendarNames.currentText() != '':
|
||||
self._settings["calendar"] = self._calendarNames.currentText()
|
||||
|
||||
if len(self._citiesWidget.selectedItems()) > 0:
|
||||
self._settings["mymuellcity"] = self._citiesWidget.selectedItems()[0].text()
|
||||
|
||||
self._dataModel.storage.settings = self._settings
|
||||
|
||||
def runnable_sync_events(self, worker: GuiWorker) -> tuple[bool, str]:
|
||||
if self._selectedCity is None:
|
||||
return False, "please select a city"
|
||||
|
||||
if self._calendarNames.currentText() == '':
|
||||
return False, "please select a calendar"
|
||||
|
||||
worker.stateChanged.emit("create calendar {} if not existent".format(self._calendarNames.currentText()))
|
||||
|
||||
self._davClient.createCalendar(self._calendarNames.currentText())
|
||||
|
||||
worker.stateChanged.emit("get events from MyMüll.de (city id {}, aread id {}".format(*self._selectedCity))
|
||||
events = self._dataModel.get_events(*self._selectedCity)
|
||||
worker.rangeChanged.emit(0, len(events))
|
||||
worker.progressChanged.emit(0)
|
||||
|
||||
for i in range(0, len(events)):
|
||||
worker.stateChanged.emit("creating event {} {}".format(events[i]["title"], events[i]["day"]))
|
||||
self._davClient.syncEvents(events[i])
|
||||
worker.progressChanged.emit(i+1)
|
||||
|
||||
worker.stateChanged.emit("syncing events finished")
|
||||
return True, "syncing events finished"
|
||||
|
||||
def runnable_delete_events(self, worker: GuiWorker) -> tuple[bool, str]:
|
||||
if self._calendarNames.currentText() == '':
|
||||
return False, "please select a calendar"
|
||||
|
||||
worker.stateChanged.emit("deleting existing events from calendar")
|
||||
|
||||
self._davClient.createCalendar(self._calendarNames.currentText())
|
||||
events = self._davClient.getExistingMuellEvents()
|
||||
if len(events) > 0:
|
||||
worker.rangeChanged.emit(0, len(events))
|
||||
worker.progressChanged.emit(0)
|
||||
else:
|
||||
worker.rangeChanged.emit(0, 1)
|
||||
worker.progressChanged.emit(1)
|
||||
|
||||
for i in range(0, len(events)):
|
||||
worker.stateChanged.emit("deleting event {}".format(events[i].vobject_instance.vevent.uid.value))
|
||||
events[i].delete()
|
||||
worker.progressChanged.emit(i+1)
|
||||
|
||||
worker.stateChanged.emit("deletion finished.")
|
||||
return True, "deleting events finished"
|
||||
|
||||
def runnable_connect_caldav(self, worker: GuiWorker) -> tuple[bool, str]:
|
||||
|
||||
|
||||
try:
|
||||
worker.stateChanged.emit("connecting to {}".format(self._url.text()))
|
||||
self._davClient.connect(self._url.text(), self._user.text(), self._password.text())
|
||||
self._calendarNames.blockSignals(True)
|
||||
for i in self._davClient.getCalendars():
|
||||
self._calendarNames.addItem(i)
|
||||
|
||||
self._calendarNames.blockSignals(False)
|
||||
self._calendarNames.setEnabled(True)
|
||||
|
||||
if self._settings["calendar"] != '':
|
||||
self._calendarNames.setCurrentText(self._settings["calendar"])
|
||||
|
||||
|
||||
|
||||
worker.stateChanged.emit("connected.")
|
||||
|
||||
|
||||
|
||||
|
||||
except Exception as e:
|
||||
worker.stateChanged.emit("connection failed.")
|
||||
return False, str(e)
|
||||
|
||||
return True, "connect successful"
|
||||
|
||||
def initUI(self):
|
||||
|
||||
tlWidget = QWidget()
|
||||
layout = QGridLayout()
|
||||
tlWidget.setLayout(layout)
|
||||
|
||||
self.setCentralWidget(tlWidget)
|
||||
|
||||
|
||||
|
||||
groupBoxMyMuell = QGroupBox("MyMüll.de Cities")
|
||||
layoutGroupBoxMyMuell = QGridLayout()
|
||||
groupBoxMyMuell.setLayout(layoutGroupBoxMyMuell)
|
||||
|
||||
groupBoxCalDav = QGroupBox("CalDAV Settings")
|
||||
layoutGroupBoxCalDav = QGridLayout()
|
||||
groupBoxCalDav.setLayout(layoutGroupBoxCalDav)
|
||||
|
||||
groupBoxProgress = QGroupBox("Progress")
|
||||
layoutGroupBoxProgress = QVBoxLayout()
|
||||
groupBoxProgress.setLayout(layoutGroupBoxProgress)
|
||||
|
||||
layoutGroupBoxMyMuell.addWidget(self._citiesWidget, 0, 0, 4, 6)
|
||||
layoutGroupBoxMyMuell.addWidget(QLabel("Filter Cities"), 4, 0, 1, 1)
|
||||
layoutGroupBoxMyMuell.addWidget(self._filterText, 4, 1, 1, 5)
|
||||
|
||||
layout.addWidget(groupBoxMyMuell)
|
||||
layout.addWidget(groupBoxCalDav)
|
||||
layout.addWidget(groupBoxProgress)
|
||||
|
||||
self.setStatusBar(self._statusBar)
|
||||
|
||||
layoutGroupBoxCalDav.addWidget(QLabel("url"), 0, 0, 1, 1)
|
||||
layoutGroupBoxCalDav.addWidget(self._url, 0, 1, 1, 5)
|
||||
layoutGroupBoxCalDav.addWidget(QLabel("username"), 1, 0, 1, 1)
|
||||
layoutGroupBoxCalDav.addWidget(self._user, 1, 1, 1, 5)
|
||||
layoutGroupBoxCalDav.addWidget(QLabel("password"), 2, 0, 1, 1)
|
||||
layoutGroupBoxCalDav.addWidget(self._password, 2, 1, 1, 5)
|
||||
layoutGroupBoxCalDav.addWidget(QLabel("calendar"), 3, 0, 1, 1)
|
||||
layoutGroupBoxCalDav.addWidget(self._calendarNames, 3, 1, 1, 5)
|
||||
|
||||
buttonLayout = QHBoxLayout()
|
||||
buttonLayout.addWidget(self._connectButton)
|
||||
buttonLayout.addWidget(self._syncButton)
|
||||
buttonLayout.addWidget(self._deleteButton)
|
||||
|
||||
layoutGroupBoxCalDav.addLayout(buttonLayout, 4, 0, 6, 6)
|
||||
|
||||
layoutGroupBoxProgress.addWidget(self._progressBar)
|
||||
layoutGroupBoxCalDav.setSpacing(0)
|
||||
layoutGroupBoxCalDav.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self._password.setEchoMode(QLineEdit.Password)
|
||||
|
||||
self._calendarNames.setEditable(True)
|
||||
|
||||
self._filterText.show()
|
||||
self._citiesWidget.show()
|
||||
|
||||
self._syncButton.setEnabled(False)
|
||||
self._calendarNames.setEnabled(False)
|
||||
|
||||
self._citiesWidget.currentItemChanged.connect(lambda cur, prev: self.entrySelected(cur))
|
||||
self._filterText.textChanged.connect(self.__fillCities)
|
||||
|
||||
self._url.textChanged.connect(self.saveSettings)
|
||||
self._user.textChanged.connect(self.saveSettings)
|
||||
self._password.textChanged.connect(self.saveSettings)
|
||||
|
||||
self._citiesWidget.itemSelectionChanged.connect(lambda: self.saveSettings(0))
|
||||
|
||||
self._workerSync.connectProgressBar(self._progressBar)
|
||||
self._workerSync.connectButton(self._syncButton)
|
||||
|
||||
self._workerConnect.connectButton(self._connectButton)
|
||||
self._workerDelete.connectButton(self._deleteButton)
|
||||
|
||||
self._workerSync.connectStatusBar(self._statusBar)
|
||||
self._workerConnect.connectStatusBar(self._statusBar)
|
||||
self._workerDelete.connectStatusBar(self._statusBar)
|
||||
self._workerDelete.connectProgressBar(self._progressBar)
|
||||
|
||||
self._workerConnect.finished.connect(self.slot_process_finished)
|
||||
self._workerSync.finished.connect(self.slot_process_finished)
|
||||
self._workerDelete.finished.connect(self.slot_process_finished)
|
||||
|
||||
self._calendarNames.currentTextChanged.connect(self.slot_calendar_selected)
|
||||
|
||||
self._progressBar.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
|
||||
groupBoxProgress.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
|
||||
groupBoxCalDav.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
|
||||
|
||||
self._url.setContentsMargins(0, 10, 0, 10)
|
||||
self._password.setContentsMargins(0, 10, 0, 10)
|
||||
self._user.setContentsMargins(0, 10, 0, 10)
|
||||
self._calendarNames.setContentsMargins(0, 10, 0, 10)
|
||||
|
||||
|
||||
#self.setGeometry(300, 300, 1000, 800)
|
||||
self.setMinimumWidth(800)
|
||||
self.setWindowTitle("MyMuell DAV GUI")
|
||||
|
||||
|
||||
|
||||
def slot_process_finished(self, result: bool, msg: str):
|
||||
if result:
|
||||
self._errorMessage.information(self, "info", msg)
|
||||
self._syncButton.setEnabled(True)
|
||||
else:
|
||||
self._errorMessage.critical(self, "error", msg)
|
||||
self._syncButton.setEnabled(False)
|
||||
|
||||
def slot_calendar_selected(self):
|
||||
|
||||
if self._calendarNames.currentText() != '' and self._davClient.is_connected:
|
||||
self.saveSettings(0)
|
||||
self._syncButton.setEnabled(True)
|
||||
else:
|
||||
self._syncButton.setEnabled(False)
|
||||
|
||||
def __fillCities(self, pattern=".+"):
|
||||
self._citiesWidget.blockSignals(True)
|
||||
self._citiesWidget.clear()
|
||||
|
||||
self._cities = self._dataModel.match_city(pattern)
|
||||
|
||||
for i in self._cities:
|
||||
c = self._dataModel.get_city_by_index(i)
|
||||
item = QListWidgetItem()
|
||||
|
||||
item.setData(QListWidgetItem.UserType, c["id"])
|
||||
item.setText(c["name"])
|
||||
|
||||
self._citiesWidget.addItem(item)
|
||||
|
||||
self._citiesWidget.blockSignals(False)
|
||||
|
||||
if self._settings["mymuellcity"] != '':
|
||||
items = self._citiesWidget.findItems(self._settings["mymuellcity"], Qt.MatchExactly)
|
||||
if len(items) > 0:
|
||||
self._citiesWidget.setCurrentItem(items[0])
|
||||
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
w = MyMuell2CalDavGui()
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
78
MyMuellDataModel.py
Normal file
78
MyMuellDataModel.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import http.client
|
||||
import json
|
||||
import LocalDataStorage
|
||||
from appdirs import *
|
||||
import logging
|
||||
import re
|
||||
|
||||
class MyMuellDataModel(object):
|
||||
MyMuellHost = 'mymuell.jumomind.com'
|
||||
log = logging.getLogger("MyMuellDataModel")
|
||||
|
||||
def __init__(self):
|
||||
self.client = http.client.HTTPSConnection(MyMuellDataModel.MyMuellHost)
|
||||
|
||||
self.storage = LocalDataStorage.LocalDataStorage()
|
||||
self.cities = self.__get_cities()
|
||||
|
||||
def get_cities_by_request(self):
|
||||
self.client.request('GET', '/mmapp/loxone/lox.php?r=cities')
|
||||
response = self.client.getresponse()
|
||||
return json.loads(str(response.read(), encoding='utf-8'))
|
||||
|
||||
def get_events(self, city_id, area_id):
|
||||
self.client.request('GET', '/mmapp/loxone/lox.php?r=dates/0&city_id={city_id}&area_id={area_id}'.format(city_id=city_id, area_id=area_id))
|
||||
response = self.client.getresponse()
|
||||
ret = str(response.read(), encoding='utf-8')
|
||||
return json.loads(ret)
|
||||
|
||||
def __get_cities(self):
|
||||
cities = self.storage.city_data
|
||||
if cities is not None:
|
||||
MyMuellDataModel.log.debug("using stored values")
|
||||
return cities
|
||||
else:
|
||||
cities = self.get_cities_by_request()
|
||||
self.storage.city_data = cities
|
||||
|
||||
return cities
|
||||
|
||||
def get_city_names(self, indices):
|
||||
ret = []
|
||||
for idx in indices:
|
||||
e = self.get_city_by_index(idx)
|
||||
if e is not None:
|
||||
ret.append(e["name"])
|
||||
return ret
|
||||
|
||||
def match_city(self, pattern):
|
||||
ret = []
|
||||
|
||||
n = 0
|
||||
for i in self.cities:
|
||||
|
||||
m = re.search(pattern, i["name"], re.IGNORECASE)
|
||||
if m is not None:
|
||||
ret.append(n)
|
||||
n = n + 1
|
||||
|
||||
return ret
|
||||
|
||||
def get_city_by_index(self, idx):
|
||||
if len(self.cities) < idx:
|
||||
return None
|
||||
|
||||
return self.cities[idx]
|
||||
|
||||
def get_city_by_id(self, id):
|
||||
for i in self.cities:
|
||||
if i["id"] == id:
|
||||
return i
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
model = MyMuellDataModel()
|
||||
matches = model.match_city("eich")
|
||||
65
README.md
Normal file
65
README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# MyMuell 2 CalDAV
|
||||
|
||||
Simple tool to retrive dates for garbage disposal from _MyMüll.de_ and sync it with a CalDAV Server, e.g. Nextcloud
|
||||
|
||||
|
||||
## Motivation
|
||||
_MyMüll.de_ is a web service / app provider which is used by some bavarian municipalities to digitally announce upcoming dates for garbage disposal.
|
||||
Although this is basically a good idea, _MyMüll.de_ does not provide any convenience functionality to import these dates in a standard calendar to use your favourite calendar app.
|
||||
|
||||
Instead, users are forced to install the buggy and heavy battery draining _MyMüll.de_ smartphone app. On some android devices (e.g. Huawei), even the notifications of the app do not work reliable by default
|
||||
(only after changing some settings related to app start policies)
|
||||
|
||||
So this tool aims to get rid of the app by parsing data from _MyMüll.de_ web service and synchronize the events with a conventional CalDAV server.
|
||||
|
||||
|
||||
## implementation status
|
||||
- all required functionality implemented for basic usage
|
||||
- functional GUI written in PyQt5
|
||||
- persist all settings for later use
|
||||
- works on Ubuntu 20.04 and Windows 10
|
||||
- works reliable in combination with Nextcloud 20 with official Calendar plugin
|
||||
|
||||
|
||||
## ToDos
|
||||
- more GUI settings
|
||||
- adjustable notification triggers for upcoming events
|
||||
- adjustable start time and event duration
|
||||
|
||||
- test/implement other CalDAV providers
|
||||
- iCloud
|
||||
- Google
|
||||
|
||||
- export the CalDAV events to a local file for manual import to a calendar
|
||||
|
||||
|
||||
#usage
|
||||
- get url of your CalDAV principal.
|
||||
For Nextcloud, this `http://your.nextcloud.host/nextcloud/remote.php/dav/calendars`
|
||||
|
||||
- Start GUI
|
||||
|
||||
|
||||
#Disclaimer
|
||||
This tool was developed by an annoyed _MyMüll.de_ app user as a free contribute
|
||||
to the open source community and is licenced under the **GPLv3** Licence.
|
||||
|
||||
This software does not stand in any relation to the official _MyMüll.de_ app or the company that provides/developes this service.
|
||||
Although this software was developed by an experienced software developer to the best of his knowledge and belief
|
||||
and was basically tested on different platforms, the author can not guarantee for the proper functionality of the software.
|
||||
|
||||
So the user uses this software at his own risk and he is completely responsible for any damage, security issues, data loss or any additional costs,
|
||||
that might occur when using this software.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
pyqt5
|
||||
caldav
|
||||
vobject
|
||||
appdirs
|
||||
cryptography
|
||||
Reference in New Issue
Block a user