diff --git a/bridges/python/Pipfile b/bridges/python/Pipfile index 5af4c007c..6c34a0665 100644 --- a/bridges/python/Pipfile +++ b/bridges/python/Pipfile @@ -8,5 +8,7 @@ requests = "==2.21.0" pytube = "==9.5.0" tinydb = "==3.9.0" beautifulsoup4 = "==4.7.1" +pyowm = "==2.10.0" +tzlocal = "==1.5.1" [dev-packages] diff --git a/bridges/python/Pipfile.lock b/bridges/python/Pipfile.lock index 54fc9aed1..3dcfd811b 100644 --- a/bridges/python/Pipfile.lock +++ b/bridges/python/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "73bde89b379ffec7cec145fa30f1ebb49c349204b730a94108151fd9b71b31ec" + "sha256": "c414207c953ddc3c5324996d81a1453f9185c7d112cfe8c704da6032badd1895" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.6" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -39,6 +37,13 @@ ], "version": "==3.0.4" }, + "geojson": { + "hashes": [ + "sha256:b175e00a76d923d6e7409de0784c147adcdd6e04b311b1d405895a4db3612c9d", + "sha256:b2bfb5c8e6b4b0c55dd139996317145aa8526146b3f8570586f9613c527a648a" + ], + "version": "==2.4.1" + }, "idna": { "hashes": [ "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", @@ -46,6 +51,14 @@ ], "version": "==2.8" }, + "pyowm": { + "hashes": [ + "sha256:2ac88777565518b9e9aa3fc172c543ada4cbfc667d83775cd17c0e52ea6ccc86", + "sha256:8fd41a18536f4d6c432bc6d9ea69994efb1ea9b43688cf19523659b6f4d86cf7" + ], + "index": "pypi", + "version": "==2.10.0" + }, "pytube": { "hashes": [ "sha256:2a32f3475f063d25e7b7a7434a93b51d59aadbbda7ed24af48f097b2876c0964", @@ -54,6 +67,14 @@ "index": "pypi", "version": "==9.5.0" }, + "pytz": { + "hashes": [ + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + ], + "index": "pypi", + "version": "==2019.1" + }, "requests": { "hashes": [ "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", @@ -77,6 +98,13 @@ "index": "pypi", "version": "==3.9.0" }, + "tzlocal": { + "hashes": [ + "sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e" + ], + "index": "pypi", + "version": "==1.5.1" + }, "urllib3": { "hashes": [ "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", diff --git a/packages/weather/.gitignore b/packages/weather/.gitignore new file mode 100644 index 000000000..29569f686 --- /dev/null +++ b/packages/weather/.gitignore @@ -0,0 +1 @@ +config/configuration25.sample.py diff --git a/packages/weather/OpenWeatherMap.py b/packages/weather/OpenWeatherMap.py new file mode 100644 index 000000000..ad0e7435a --- /dev/null +++ b/packages/weather/OpenWeatherMap.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- + +import functools + +import utils + +from tzlocal import get_localzone +from pyowm import OWM + + +# Decorators + +def load_config(func): + @functools.wraps(func) + def wrapper_load_config(string, entities): + payload = dict() + payload["string"] = string + payload["entities"] = entities + + api_key = utils.config("api_key") + pro = utils.config("pro") + payload["temperature_units"] = utils.config("temperature_units") + payload["wind_speed_units"] = utils.config("wind_speed_units") + + if ( + (payload["temperature_units"] != "celsius") + and (payload["temperature_units"] != "fahrenheit") + ): + return utils.output( + "end", + "invalid_temperature_units", + utils.translate("invalid_temperature_units") + ) + + if payload["wind_speed_units"] == "meters per seconds": + payload["wind_speed_units_response"] = payload["wind_speed_units"] + payload["wind_speed_units"] = "meters_sec" + elif payload["wind_speed_units"] == "miles per hour": + payload["wind_speed_units_response"] = payload["wind_speed_units"] + payload["wind_speed_units"] = "miles_hour" + else: + return utils.output( + "end", + "invalid_wind_speed_units", + utils.translate("invalid_wind_speed_units") + ) + + if pro: + payload["owm"] = OWM(api_key, subscription_type="pro") + else: + payload["owm"] = OWM(api_key) + + return func(payload) + return wrapper_load_config + + +def acquire_weather(func): + @functools.wraps(func) + def wrapper_acquire_weather(payload): + for item in payload["entities"]: + if item["entity"] == "city": + utils.output( + "inter", + "acquiring", + utils.translate("acquiring") + ) + + payload["city"] = item["sourceText"].title() + payload["observation"] = payload["owm"].weather_at_place(payload["city"]) + payload["wtr"] = payload["observation"].get_weather() + + return func(payload) + return utils.output( + "end", + "request_error", + utils.translate("request_error") + ) + + return wrapper_acquire_weather + + +# Methods + +@load_config +@acquire_weather +def current_weather(payload): + """ + Get the current weather. + """ + + detailed_status = payload["wtr"].get_detailed_status() + temperatures = payload["wtr"].get_temperature(payload["temperature_units"]) + humidity = payload["wtr"].get_humidity() + + return utils.output( + "end", + "current_weather", + utils.translate( + "current_weather", + { + "detailed_status": detailed_status.capitalize(), + "city": payload["city"], + "temperature": temperatures["temp"], + "temperature_units": payload["temperature_units"].capitalize(), + "humidity": humidity + } + ) + ) + + +@load_config +@acquire_weather +def temperature(payload): + """ + Get the current temperature. + """ + + temperatures = payload["wtr"].get_temperature(payload["temperature_units"]) + + return utils.output( + "end", + "temperature", + utils.translate( + "temperature", + { + "city": payload["city"], + "temperature": temperatures["temp"], + "temperature_units": payload["temperature_units"].capitalize() + } + ) + ) + + +@load_config +@acquire_weather +def humidity(payload): + """ + Get the current humidity. + """ + + humidity = payload["wtr"].get_humidity() + + return utils.output( + "end", + "humidity", + utils.translate( + "humidity", + { + "city": payload["city"], + "humidity": humidity + } + ) + ) + + +@load_config +@acquire_weather +def wind(payload): + """ + Get the current wind speed and direction. + """ + + wind = payload["wtr"].get_wind(payload["wind_speed_units"]) + + return utils.output( + "end", + "wind", + utils.translate( + "wind", + { + "city": payload["city"], + "wind_speed": wind["speed"], + "wind_speed_units_response": payload["wind_speed_units_response"], + "wind_direction": wind["deg"] + } + ) + ) + + +@load_config +@acquire_weather +def sunrise(payload): + """ + Get when the sun rises. + """ + + dt = payload["wtr"].get_sunrise_time("date") + dt = dt.astimezone(get_localzone()) + + return utils.output( + "end", + "sunrise", + utils.translate( + "sunrise", + {"time": dt.strftime("%H:%M:%S"), "city": payload["city"]} + ) + ) + + +@load_config +@acquire_weather +def sunset(payload): + """ + Get when the sun sets. + """ + + dt = payload["wtr"].get_sunset_time("date") + dt = dt.astimezone(get_localzone()) + + return utils.output( + "end", + "sunset", + utils.translate( + "sunset", + {"time": dt.strftime("%H:%M:%S"), "city": payload["city"]} + ) + ) diff --git a/packages/weather/README.md b/packages/weather/README.md new file mode 100644 index 000000000..1e032897a --- /dev/null +++ b/packages/weather/README.md @@ -0,0 +1,49 @@ +# Weather Package + +The weather package contains modules which include getting the latest weather forecast. + +## Modules + +### OpenWeatherMap + +#### Requirements +- tzlocal +- PyOWM + +#### Usage + +1. Register a new account on [OpenWeatherMap Sign Up](https://openweathermap.org/sign_up) unless you already have one. +2. Generate a new API key on [OpenWeatherMap API keys](https://home.openweathermap.org/api_keys). +3. Duplicate the file `packages/weather/config/config.sample.json` and rename it `config.json`. +4. Copy the API key in `packages/weather/config/config.json` and set the other options to your liking. +5. This package uses PyOWM. To install it: from inside leon directory `cd bridges/python` +6. `pipenv install tzlocal pyowm` +7. Done! + +``` +(en-US) +- "What's the weather like in Milan?" +- "When is the sun going to set in Rome?" +- "Is it windy in Chicago?" + +(it-IT) +- "Che tempo fa a Milano?" +- "Quando tramonta il sole a Roma?" +... +``` + +#### Options +- `pro`: Set this to `true` if you have a premium subscription. +- `temperature_units`: Choose which temperature scale to use. ["celsius", "fahrenheit"] +- `wind_speed_units`: Choose which units to use for the wind speed. ["meters per second", "miles per hour"] More units coming soon. + +#### Links + +- [OpenWeatherMap API](https://developers.google.com/youtube/v3/getting-started) +- [PyOWM GitHub](https://github.com/csparpa/pyowm) +- [PyOWM Docs](https://pyowm.readthedocs.io/en/latest/) + +#### TODO +- Implement some sort of caching mechanism. PyOWM does support caching, but it's a bit messy. It might be easier to implement it from scratch, perhaps with some sort of db like TinyDb which is already supported by leon. +- Implement `pro` functionality for OWM. +- Add more weather services and related (UV, pollution, sky events, etc.). diff --git a/packages/weather/__init__.py b/packages/weather/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/weather/config/.gitkeep b/packages/weather/config/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/weather/config/config.sample.json b/packages/weather/config/config.sample.json new file mode 100644 index 000000000..04c721a53 --- /dev/null +++ b/packages/weather/config/config.sample.json @@ -0,0 +1,9 @@ +{ + "OpenWeatherMap": { + "api_key": "YOUR_OPEN_WEATHER_MAP_TOKEN", + "pro": false, + "temperature_units": "celsius", + "wind_speed_units": "meters per seconds", + "options": {} + } +} diff --git a/packages/weather/data/.gitkeep b/packages/weather/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/weather/data/answers/.gitkeep b/packages/weather/data/answers/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/weather/data/answers/en.json b/packages/weather/data/answers/en.json new file mode 100644 index 000000000..fc3e04003 --- /dev/null +++ b/packages/weather/data/answers/en.json @@ -0,0 +1,41 @@ +{ + "OpenWeatherMap": { + "acquiring": [ + "I'll take a look.", + "Let me see..." + ], + "current_weather": [ + "%detailed_status% over %city%. It's %temperature% degree %temperature_units%. Humidity is %humidity% percent." + ], + "temperature": [ + "It's %temperature% degree %temperature_units% in %city%." + ], + "humidity": [ + "The humidity in %city% is %humidity% percent." + ], + "wind": [ + "The wind in %city% is blowing at %wind_speed% %wind_speed_units_response% at %wind_direction% degree." + ], + "sunrise": [ + "Today the sun rises at %time% in %city%." + ], + "sunset": [ + "Today the sun sets set at %time% in %city%." + ], + "invalid_temperature_units": [ + "Invalid temperature units. Check your config.json file." + ], + "invalid_wind_speed_units": [ + "Invalid wind speed units. Check your config.json file." + ], + "settings_error": [ + "Please check your settings. An error occured because %reason%. The message is: %message%." + ], + "request_error": [ + "I could not understand your request." + ], + "connection_error": [ + "I'm having issuse reaching the server." + ] + } +} diff --git a/packages/weather/data/expressions/.gitkeep b/packages/weather/data/expressions/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/weather/data/expressions/en.json b/packages/weather/data/expressions/en.json new file mode 100644 index 000000000..3078f87f6 --- /dev/null +++ b/packages/weather/data/expressions/en.json @@ -0,0 +1,147 @@ +{ + "OpenWeatherMap": { + "current_weather": { + "expressions": [ + "What's the weather like in" + ], + "entities": [ + { + "type": "trim", + "name": "city", + "conditions": [ + { + "type": "after_last", + "from": "in" + }, + { + "type": "between", + "from": "in", + "to": "?" + } + ] + } + ] + }, + "temperature": { + "expressions": [ + "Is it cold in", + "How cold is it in", + "Is it hot in", + "How hot is it in", + "Is it warm in", + "How warm is it in", + "What's the temperature like in" + ], + "entities": [ + { + "type": "trim", + "name": "city", + "conditions": [ + { + "type": "after_last", + "from": "in" + }, + { + "type": "between", + "from": "in", + "to": "?" + } + ] + } + ] + }, + "humidity": { + "expressions": [ + "Is it humid in", + "What is the humidity in" + ], + "entities": [ + { + "type": "trim", + "name": "city", + "conditions": [ + { + "type": "after_last", + "from": "in" + }, + { + "type": "between", + "from": "in", + "to": "?" + } + ] + } + ] + }, + "wind": { + "expressions": [ + "Is it windy in" + ], + "entities": [ + { + "type": "trim", + "name": "city", + "conditions": [ + { + "type": "after_last", + "from": "in" + }, + { + "type": "between", + "from": "in", + "to": "?" + } + ] + } + ] + }, + "sunrise": { + "expressions": [ + "When is the sun going to rise in", + "When does the sun rise in", + "When did the sun rise in" + ], + "entities": [ + { + "type": "trim", + "name": "city", + "conditions": [ + { + "type": "after_last", + "from": "in" + }, + { + "type": "between", + "from": "in", + "to": "?" + } + ] + } + ] + }, + "sunset": { + "expressions": [ + "When is the sun going to set in", + "When does the sun set in", + "When did the sun set in" + ], + "entities": [ + { + "type": "trim", + "name": "city", + "conditions": [ + { + "type": "after_last", + "from": "in" + }, + { + "type": "between", + "from": "in", + "to": "?" + } + ] + } + ] + } + } +} diff --git a/packages/weather/version.txt b/packages/weather/version.txt new file mode 100644 index 000000000..21e8796a0 --- /dev/null +++ b/packages/weather/version.txt @@ -0,0 +1 @@ +1.0.3