diff --git a/wetteronline/WetterOnline/Pipfile b/wetteronline/WetterOnline/Pipfile new file mode 100644 index 0000000..c98f658 --- /dev/null +++ b/wetteronline/WetterOnline/Pipfile @@ -0,0 +1,14 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +lxml = "*" + +[requires] + diff --git a/wetteronline/WetterOnline/__init__.py b/wetteronline/WetterOnline/__init__.py new file mode 100644 index 0000000..96ac5bb --- /dev/null +++ b/wetteronline/WetterOnline/__init__.py @@ -0,0 +1,118 @@ +import logging +import urllib.parse +import re +import requests + +from lxml import html + + +class WetterOnline: + def __init__(self, location): + self.logger = logging.getLogger(__name__) + self.location = None + self.weather = None + self.url = self.get_url(location) + + def get_url(self, location): + try: + url = "http://api.wetteronline.de/search?name={}".format( + urllib.parse.quote(location.lower()) + ) + rq = self._fetch_data(url, headers={"content-type": "application/json"}) + res = rq.json() + except Exception: + self.logger.exception("Failed searching location %s", location) + + for x in res: + if "match" in x and x["match"] == "yes": + self.location = res[0]["geoName"] + + if not self.location: + raise Exception("Location not found!") + + return "http://www.wetteronline.de/{}/".format(self.location) + + def get(self): + try: + rq = self._fetch_data(self.url) + tree = html.fromstring(rq.content) + weather = self._parse_tree(tree) + except Exception: + self.logger.exception("Failed getting weather") + + return {"location": self.location, "weather": weather} + + def _fetch_data(self, url, headers=None): + try: + self.logger.debug("Fetching: %s", url) + rq = requests.get(url, headers=headers, verify=False) + except Exception: + self.logger.exception("Failed fetching data") + return + return rq + + def _parse_tree(self, tree): + weather = [] + try: + for day in range(1, 4): + daystr = tree.xpath( + f'//table[@id="daterow"]/tbody/tr/th[{day}]/text()' + )[-1].strip() + # Workaround 'today' becoming 'tomorrow' too early on the site + if day == 1 and daystr.lower() != "heute": + break + w = { + "day": daystr, + "date": re.search( + r"(\d+\.\d+\.)", + tree.xpath( + f'//table[@id="daterow"]/tbody/tr/th[{day}]/span/text()' + )[-1].strip(), + ).group(0), + "temp_max": float( + tree.xpath( + f'//table[@id="weather"]/tbody/tr[@class="Maximum Temperature"]/td[{day}]/div/span[2]/text()' + )[-1].replace("°", "") + ), + "temp_min": float( + tree.xpath( + f'//table[@id="weather"]/tbody/tr[@class="Minimum Temperature"]/td[{day}]/div/span[2]/text()' + )[-1].replace("°", "") + ), + "sunhours": float( + re.search( + r"(\d+)", + tree.xpath( + f'//tr[@id="sun_teaser"]/td[{day}]/span[1]/text()' + )[-1], + ).group(0) + ), + "rain_probability": float( + re.search( + r"(\d+)", + tree.xpath( + f'//tr[@id="precipitation_teaser"]/td[{day}]/span[1]/text()' + )[-1], + ).group(0) + ), + "src": tree.xpath(f'//tr[@id="wwdaysymbolrow"]/td[{day}]/img/@src')[ + -1 + ], + "img": "question", + "title": tree.xpath( + f'//tr[@id="wwdaysymbolrow"]/td[{day}]/@data-tt-args' + )[-1] + .split(",")[2] + .replace('"', ""), + } + if day == 1: + w["temp_now"] = float( + tree.xpath( + '//div[@id="nowcast-card-temperature"]/div[1]/text()' + )[-1] + ) + weather.append(w) + except Exception: + self.logger.exception("Failed parsing data") + return + return weather diff --git a/wetteronline/__init__.py b/wetteronline/__init__.py new file mode 100644 index 0000000..1647829 --- /dev/null +++ b/wetteronline/__init__.py @@ -0,0 +1,102 @@ +import lib.plugin +import re +from pprint import pformat +from wetteronline.WetterOnline import WetterOnline + +icons = { + "so": "sun", + "mo": "moon", + "ns": "weather.fog", + "nm": "weather.fog", + "nb": "weather.fog", + "wb": "weather.partlysunny", + "mb": "weather.partlysunny_n", + "bd": "weather.cloudy", + "wbs1": "weather.partlysunny", + "mbs1": "weather.partlysunny_n", + "bdr1": "weather.rain1", + "bds1": "weather.rain1", + "wbr1": "weather.rain2_shower", + "wbr2": "weather.rain2_shower", + "wbs2": "weather.rain2_shower", + "mbs2": "weather.rain2", + "bdr2": "weather.rain2", + "bdr3": "weather.rain3", + "wbr3": "weather.rain3_shower", + "wbsrs1": "weather.snowrain2_shower", + "mbsrs1": "weather.snowrain1", + "bdsr1": "weather.snowrain1", + "wbsrs2": "weather.snowrain3_shower", + "mbsrs2": "weather.snowrain2", + "bdsr2": "weather.snowrain2", + "bdsr3": "weather.snowrain3", + "wbsns1": "weather.snow2_shower", + "mbsns1": "weather.snow1", + "bdsn1": "weather.snow1", + "wbsns2": "weather.snow3_shower", + "mbsns2": "weather.snow2", + "bdsn2": "weather.snow2", + "bdsn3": "weather.snow3", + "wbsg": "weather.storm1", + "mbsg": "weather.storm1", + "bdsg": "weather.storm1", + "wbg1": "weather.storm1", + "mbg1": "weather.storm1", + "bdg1": "weather.storm1", + "wbg2": "weather.storm2", + "mbg2": "weather.storm2", + "bdg2": "weather.storm2", + "bdgr1": "weather.graupel2", + "bdgr2": "weather.graupel3", +} + + +class WetterOnlinePlugin(lib.plugin.Plugin): + def __init__(self, core, conf): + lib.plugin.Plugin.__init__(self, core, conf) + self.location = "{}".format(conf.get("location")) + self.items = {} + core.scheduler.add( + "_wetteronline", self.update, cycle=int(conf.get("cycle", 900)) + ) + + def start(self): + self.wol = WetterOnline(self.location) + self.alive = True + + def stop(self): + self.alive = False + + def pre_stage(self): + for node in self._core.config.query_nodes("wetteronline"): + keyword = node.attr["wetteronline"] + self.items[keyword] = node + + def update(self, value=None, trigger=None): + try: + wol_data = self.wol.get() + self.logger.info( + "Successfully fetched WetterOnline data for %i days", + len(wol_data["weather"]), + ) + except Exception: + self.logger.exception("Failed getting WetterOnline data") + return + + # Map native icons + for wd in wol_data["weather"]: + img = re.search(r"/([^/_]+)_*\.(svg|png)$", wd["src"]) + if img: + img = img.group(1) + if img in icons: + wd["img"] = icons[img] + + for keyword, item in self.items.items(): + try: + (wol_idx, wol_key) = keyword.split("__") + value = wol_data["weather"][int(wol_idx)][wol_key] + item(value, trigger=self.get_trigger()) + except: + self.logger.exception( + "Unable to update item %s with value %s" % (item, value) + ) diff --git a/wetteronline/config.md b/wetteronline/config.md new file mode 100644 index 0000000..807c302 --- /dev/null +++ b/wetteronline/config.md @@ -0,0 +1,9 @@ +{{ + +form.guiInput('location', label='Ort', required=True, value='Berlin', help="""Ortsname wie er für das HTML-Widget von WetterOnline funktioniert (https://www.wetteronline.de/wetter-widget)""") + +select_cycle = oDict([('60', '1 Minute'), ('300', '5 Minuten'), ('900', '15 Minuten'), ('1800', '30 Minuten'), ('3600', '1 Stunde')]) +form.guiSelect('cycle', label='Abfrageintervall', named=select_cycle) + +}} + diff --git a/wetteronline/item.md b/wetteronline/item.md new file mode 100644 index 0000000..4f69ecc --- /dev/null +++ b/wetteronline/item.md @@ -0,0 +1,32 @@ +{{ select_data = oDict([ + ('', ''), + ('_0', '== Heute =='), + ('0__title', 'Wetterlage'), + ('0__img', 'Icon'), + ('0__src', 'Icon (orig)'), + ('0__temp_now', 'Temperatur aktuell (°C)'), + ('0__temp_min', 'Temperatur min. (°C)'), + ('0__temp_max', 'Temperatur max. (°C)'), + ('0__sunhours', 'Sonnenstunden (h)'), + ('0__rain_probability', 'Regenwahrscheinlichkeit (%)'), + + ('_1', '== Morgen =='), + ('1__title', 'Wetterlage'), + ('1__img', 'Icon'), + ('1__src', 'Icon (orig)'), + ('1__temp_min', 'Temperatur min. (°C)'), + ('1__temp_max', 'Temperatur max. (°C)'), + ('1__sunhours', 'Sonnenstunden (h)'), + ('1__rain_probability', 'Regenwahrscheinlichkeit (%)'), + + ('_2', '== Übermorgen =='), + ('2__title', 'Wetterlage'), + ('2__img', 'Icon'), + ('2__src', 'Icon (orig)'), + ('2__temp_min', 'Temperatur min. (°C)'), + ('2__temp_max', 'Temperatur max. (°C)'), + ('2__sunhours', 'Sonnenstunden (h)'), + ('2__rain_probability', 'Regenwahrscheinlichkeit (%)'), +]) +form.guiSelect('wetteronline', label='Wert', named=select_data) +}} diff --git a/wetteronline/plugin.conf b/wetteronline/plugin.conf new file mode 100644 index 0000000..b073ee6 --- /dev/null +++ b/wetteronline/plugin.conf @@ -0,0 +1,2 @@ +[WetterOnline] + class = WetterOnlinePlugin