mirror of
https://github.com/oasis6212/Meshbot_weather.git
synced 2025-12-10 00:06:12 -06:00
August 2025 update 3
This commit is contained in:
parent
86a026651e
commit
ddc28caba7
100
README.md
100
README.md
@ -36,8 +36,9 @@
|
||||

|
||||
|
||||
|
||||
MeshBot Weather is a spinoff of [MeshBot](https://github.com/868meshbot/meshbot) with a detailed focus on weather. Designed to run on a computer or a
|
||||
Raspberry Pi with a connected Meshtastic device.
|
||||
[MeshBot Weather](https://github.com/oasis6212/Meshbot_weather) is a spinoff of [MeshBot](https://github.com/868meshbot/meshbot) that brings you
|
||||
accurate, real-time forecasts and instant weather alerts. Designed to run on a computer or a
|
||||
Raspberry Pi with a connected Meshtastic radio.
|
||||
|
||||
Our Mission:
|
||||
|
||||
@ -62,9 +63,10 @@ Our Mission:
|
||||
- Forecasts are generated for any location. Not limited to towns or cities.
|
||||
- Optional firewall, when enabled, the bot will only respond to messages from nodes that have been included in its whitelist.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Thanks [davidfries](https://github.com/davidfries)!
|
||||
|
||||
@ -182,6 +184,16 @@ cd meshbot_weather
|
||||
|
||||
See above under "How to run the program on various operating systems."
|
||||
|
||||
## Location setup for alerts and forecast
|
||||
You will need to edit the settings.yaml file. Look for:
|
||||
|
||||
ALERT_LAT: "37.7654"
|
||||
|
||||
ALERT_LON: "-100.0151"
|
||||
|
||||
Change these coordinates to match the location you want weather info and alerts for. Do not use more than four digits
|
||||
past the decimal point.
|
||||
|
||||
## Configuration
|
||||
|
||||
The ''settings.yaml'' file; it's where you can configure different options. Can be edited in notepad.
|
||||
@ -195,18 +207,19 @@ MYNODES:
|
||||
FIREWALL: false
|
||||
DM_MODE: true
|
||||
DUTYCYCLE: false
|
||||
NWS_OFFICE: "BGM"
|
||||
NWS_GRID_X: "84"
|
||||
NWS_GRID_Y: "89"
|
||||
ALERT_LAT: "37.7654"
|
||||
ALERT_LON: "-100.0151"
|
||||
NWS_OFFICE: ""
|
||||
NWS_GRID_X: ""
|
||||
NWS_GRID_Y: ""
|
||||
ALERT_CHECK_INTERVAL: 300
|
||||
ALERT_INCLUDE_DESCRIPTION:
|
||||
ALERT_CHANNEL_INDEX: 0
|
||||
FIRST_MESSAGE_DELAY: 0
|
||||
MESSAGE_DELAY: 15
|
||||
ENABLE_FULL_ALERT_COMMAND: true
|
||||
ENABLE_CUSTOM_LOOKUP: true
|
||||
ENABLE_ALERT_COMMAND: true
|
||||
SHOW_ALERT_COMMAND_IN_MENU: false
|
||||
SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU: false
|
||||
ENABLE_7DAY_FORECAST: true
|
||||
ENABLE_5DAY_FORECAST: true
|
||||
ENABLE_HOURLY_WEATHER: true
|
||||
@ -235,11 +248,13 @@ Description
|
||||
- DUTYCYCLE: false: If true, limits itself to 10% Dutycycle
|
||||
|
||||
|
||||
- NWS_OFFICE: NWS_GRID_X: NWS_GRID_Y: #settings for the weather forecast api calls, see below to learn how to set up.
|
||||
- ALERT_LAT: "34.0522" ALERT_LON: "-118.2433" # Location settings for alerts and forecast, put in the latitude and
|
||||
longitude of the area you want coverage for. Make sure you only go up to 4 places past the decimal point on each.
|
||||
|
||||
|
||||
- ALERT_LAT: "34.0522" ALERT_LON: "-118.2433" #settings for alerts, put in the latitude, and longitude of the area you
|
||||
want alerts for. Make sure you only go up to 4 places past the decimal point on each.
|
||||
- NWS_OFFICE: NWS_GRID_X: NWS_GRID_Y: #Can be left blank. These settings are used for manual entry of the weather
|
||||
forecast api parameters. May be useful if you want your forecast generation for a different area than your alerts or if
|
||||
the automatic configuration fails. See below for more info.
|
||||
|
||||
|
||||
- ALERT_CHECK_INTERVAL: # Time in seconds. How often the alert API is called. NWS does not publish allowable limits.
|
||||
@ -247,7 +262,7 @@ From what I have gathered, they allow up to once a minute for alert checking. Yo
|
||||
|
||||
|
||||
- ALERT_INCLUDE_DESCRIPTION: #Set to false to exclude description from alerts. Descriptions will include alot of detail
|
||||
such as every county, town, and area affected. You can expect about 4 or 5 messages when description is set to "true" vs
|
||||
such as every county, town, and area affected. You can expect about 4 to 8 messages when description is set to "true" vs
|
||||
a single message when set to false.
|
||||
|
||||
|
||||
@ -259,21 +274,24 @@ experimental. Hoping this may help with dropped 1st part of reply's, by giving t
|
||||
feel free to experiment with different values.
|
||||
|
||||
|
||||
- MESSAGE_DELAY: Delay in seconds between split messages. To short of a delay can cause messages to arrive out of order.
|
||||
- MESSAGE_DELAY: # Delay in seconds between split messages. To short of a delay can cause messages to arrive out of order.
|
||||
|
||||
|
||||
- ENABLE_FULL_ALERT_COMMAND: #set to false to disable the "alert" command. Can produce up to 8 messages, may want to
|
||||
disable on a high traffic mesh.
|
||||
- ENABLE_ALERT_COMMAND: # Set to false to disable the alert request command, automatic alerts will not be affected.
|
||||
|
||||
|
||||
- ENABLE_CUSTOM_LOOKUP: # Enable/disable custom lat/lon lookup via message. More info below.
|
||||
- SHOW_ALERT_COMMAND_IN_MENU: # When false, hides the command from the menu but keeps it enabled, if enabled.
|
||||
|
||||
|
||||
- SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU: # Set to false to hide the custom lookup command from the menu. Command is always
|
||||
accessible.
|
||||
|
||||
|
||||
- ENABLE_7DAY_FORECAST: ENABLE_5DAY_FORECAST: ENABLE_HOURLY_WEATHER: # These calls produce 2 to 4 messages each. If you
|
||||
are on a high-traffic mesh, you may want to disable these.
|
||||
|
||||
|
||||
- FULL_MENU: true # When true, includes all weather commands. When false, shows only forecast options that return a
|
||||
- FULL_MENU: # When true, includes all weather commands. When false, shows only forecast options that return a
|
||||
single message.
|
||||
|
||||
|
||||
@ -303,7 +321,33 @@ unique the better. This is what NWS uses instead of an API key.
|
||||
Gives you the opportunity to fix the issue and stop getting throttled.
|
||||
|
||||
|
||||
## How to get your NWS_OFFICE, NWS_GRID_X, and NWS_GRID_Y
|
||||
|
||||
## Closing the program
|
||||
|
||||
Press "Ctrl + c" once to tell the program to close. If Node shutdown is enabled in the settings.yaml The program will
|
||||
command the node to shutdown and give it time to do so.
|
||||
|
||||
Pressing "Ctrl + c" twice will force a hard exit of the program.
|
||||
|
||||
|
||||
## Using the "Loc" custom location lookup command.
|
||||
The loc command allows you to get a forecast for an area that is not the bots primary location. Input the locations
|
||||
latitude and longitude along with the forecast type you want.
|
||||
|
||||
Full command example: "loc 39.0453/-98.2077 hourly"
|
||||
|
||||
Structure: loc {Latitude/longitude Command} command can be any of the regular commands like wind, 2day, 7day etc.
|
||||
To ensure compatibility of your coordinates, only use up to 4 digits past the decimal point like in the example.
|
||||
|
||||
|
||||
## Advance setup: How to get your NWS_OFFICE, NWS_GRID_X, and NWS_GRID_Y
|
||||
|
||||
|
||||
Note: As of the latest update, these values are automatically set based on the ALERT_LAT and ALERT_LON coordinates.
|
||||
Leave the grid parameters blank to enable automatic configuration. Only enter grid coordinates if you want to override
|
||||
the automatic settings.
|
||||
|
||||
|
||||
To get your NWS office and grid coordinates:
|
||||
1. Go to (https://weather.gov)
|
||||
2. Enter your address
|
||||
@ -327,24 +371,6 @@ For the alert settings in the settings.yaml file, enter your gps coordinates or
|
||||
earlier in this process. Use no more than four digits after the decimal point.
|
||||
|
||||
|
||||
## Closing the program
|
||||
|
||||
Press "Ctrl + c" once to tell the program to close. If Node shutdown is enabled in the settings.yaml The program will
|
||||
command the node to shutdown and give it time to do so.
|
||||
|
||||
Pressing "Ctrl + c" twice will force a hard exit of the program.
|
||||
|
||||
|
||||
## Using the "Loc" custom location lookup command.
|
||||
The loc command allows you to get a forecast for an area that is not the bots primary location. Input the locations
|
||||
latitude and longitude along with the forecast type you want.
|
||||
|
||||
Full command example: "loc 39.0453/-98.2077 hourly"
|
||||
|
||||
Structure: loc {Latitude/longitude Command} command can be any of the regular commands like wind, 2day, 7day etc.
|
||||
To ensure compatibility of your coordinates, only use up to 4 digits past the decimal point like in the example.
|
||||
|
||||
|
||||
## API Handling details
|
||||
|
||||
To prevent excessive api calls, the bot will check if it currently has the data being requested and if it is
|
||||
@ -388,6 +414,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
||||
This project is neither endorsed by nor supported by Meshtastic.
|
||||
|
||||
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various
|
||||
|
||||
licenses, see GitHub for details. No warranty is provided - use at your own risk.
|
||||
|
||||
|
||||
BIN
img/Automaticgrid.png
Normal file
BIN
img/Automaticgrid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 334 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 322 KiB |
134
meshbot.py
134
meshbot.py
@ -34,6 +34,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
print(r"""
|
||||
-----------------Welcome to Meshbot Weather------------------
|
||||
""")
|
||||
|
||||
|
||||
|
||||
@ -59,6 +62,89 @@ except ImportError:
|
||||
import serial.tools.list_ports
|
||||
import requests
|
||||
|
||||
|
||||
|
||||
def infer_nws_grid_from_coords(settings, logger=None):
|
||||
"""
|
||||
Fill in NWS_OFFICE, NWS_GRID_X, NWS_GRID_Y using ALERT_LAT and ALERT_LON.
|
||||
Only runs if any NWS_* value is missing or empty. Updates settings in-place.
|
||||
Returns True if values were inferred and updated; otherwise False.
|
||||
"""
|
||||
# If already complete, nothing to do
|
||||
have_office = bool(str(settings.get("NWS_OFFICE", "")).strip())
|
||||
have_x = bool(str(settings.get("NWS_GRID_X", "")).strip())
|
||||
have_y = bool(str(settings.get("NWS_GRID_Y", "")).strip())
|
||||
if have_office and have_x and have_y:
|
||||
office = str(settings.get("NWS_OFFICE", "")).strip()
|
||||
grid_x = str(settings.get("NWS_GRID_X", "")).strip()
|
||||
grid_y = str(settings.get("NWS_GRID_Y", "")).strip()
|
||||
msg = f"Using NWS grid from settings.yaml: office={office}, grid=({grid_x}, {grid_y})"
|
||||
logger_obj = globals().get("logger")
|
||||
if logger_obj:
|
||||
logger_obj.info(msg)
|
||||
else:
|
||||
print(msg)
|
||||
return False
|
||||
|
||||
# Need lat/lon for inference
|
||||
lat_raw = settings.get("ALERT_LAT")
|
||||
lon_raw = settings.get("ALERT_LON")
|
||||
|
||||
|
||||
if not lat_raw or not lon_raw:
|
||||
if logger:
|
||||
logger.warning("Cannot determine NWS grid: ALERT_LAT/ALERT_LON not set.")
|
||||
return False
|
||||
|
||||
# Prepare request
|
||||
try:
|
||||
lat = float(lat_raw)
|
||||
lon = float(lon_raw)
|
||||
except (TypeError, ValueError):
|
||||
if logger:
|
||||
logger.warning("Cannot infer NWS grid: ALERT_LAT/ALERT_LON are not valid numbers.")
|
||||
return False
|
||||
|
||||
lat_s = f"{lat:.4f}"
|
||||
lon_s = f"{lon:.4f}"
|
||||
url = f"https://api.weather.gov/points/{lat_s},{lon_s}"
|
||||
|
||||
user_agent_app = str(settings.get("USER_AGENT_APP", "meshbot-weather"))
|
||||
user_agent_email = str(settings.get("USER_AGENT_EMAIL", "contact@example.com"))
|
||||
headers = {
|
||||
"Accept": "application/geo+json",
|
||||
"User-Agent": f"({user_agent_app}, {user_agent_email})",
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
props = data.get("properties", {}) or {}
|
||||
office = props.get("gridId")
|
||||
grid_x = props.get("gridX")
|
||||
grid_y = props.get("gridY")
|
||||
|
||||
if not office or grid_x is None or grid_y is None:
|
||||
if logger:
|
||||
logger.warning("NWS Points API did not return grid info; leaving NWS_* unchanged.")
|
||||
return False
|
||||
|
||||
# Update as strings to match settings.yaml style
|
||||
settings["NWS_OFFICE"] = str(office)
|
||||
settings["NWS_GRID_X"] = str(grid_x)
|
||||
settings["NWS_GRID_Y"] = str(grid_y)
|
||||
|
||||
if logger:
|
||||
logger.info(f"NWS grid auto-config: office={office}, x={grid_x}, y={grid_y}")
|
||||
return True
|
||||
|
||||
except requests.RequestException as e:
|
||||
if logger:
|
||||
logger.warning(f"Failed to fetch NWS grid from Points API: {e}")
|
||||
return False
|
||||
|
||||
|
||||
from modules.temperature_24hour import Temperature24HourFetcher
|
||||
from modules.forecast_2day import Forecast2DayFetcher
|
||||
from modules.hourly_weather import EmojiWeatherFetcher
|
||||
@ -107,11 +193,22 @@ alerts = None
|
||||
with open("settings.yaml", "r") as file:
|
||||
settings = yaml.safe_load(file)
|
||||
|
||||
ALERT_LAT = settings.get("ALERT_LAT")
|
||||
ALERT_LON = settings.get("ALERT_LON")
|
||||
|
||||
|
||||
logger.info(f"ALERT_LAT:{ALERT_LAT} ALERT_LON:{ALERT_LON}")
|
||||
|
||||
|
||||
infer_nws_grid_from_coords(settings, logger=logger if 'logger' in globals() else None)
|
||||
|
||||
MYNODES = settings.get("MYNODES")
|
||||
DM_MODE = settings.get("DM_MODE")
|
||||
FIREWALL = settings.get("FIREWALL")
|
||||
DUTYCYCLE = settings.get("DUTYCYCLE")
|
||||
|
||||
|
||||
|
||||
NWS_OFFICE = settings.get("NWS_OFFICE", "HNX")
|
||||
NWS_GRID_X = settings.get("NWS_GRID_X", "67")
|
||||
NWS_GRID_Y = settings.get("NWS_GRID_Y", "80")
|
||||
@ -120,9 +217,14 @@ USER_AGENT_APP = settings.get("USER_AGENT_APP", "myweatherapp")
|
||||
USER_AGENT_EMAIL = settings.get("USER_AGENT_EMAIL", "contact@example.com")
|
||||
USER_AGENT = f"({USER_AGENT_APP}, {USER_AGENT_EMAIL})"
|
||||
|
||||
logger.info(f"DUTYCYCLE: {DUTYCYCLE}")
|
||||
logger.info(f"DM_MODE: {DM_MODE}")
|
||||
logger.info(f"FIREWALL: {FIREWALL}")
|
||||
|
||||
|
||||
#logger.info(f"DUTYCYCLE: {DUTYCYCLE}")
|
||||
#logger.info(f"DM_MODE: {DM_MODE}")
|
||||
#logger.info(f"FIREWALL: {FIREWALL}")
|
||||
#logger.info(f"MYNODES: {MYNODES}")
|
||||
|
||||
|
||||
|
||||
transmission_count = 0
|
||||
cooldown = False
|
||||
@ -336,10 +438,20 @@ def message_listener(packet, interface):
|
||||
"rain - 24h precipitation\n" \
|
||||
"temp - 24h temperature\n"
|
||||
# Add alert command if enabled
|
||||
if settings.get('ENABLE_FULL_ALERT_COMMAND', True):
|
||||
if settings.get("ENABLE_ALERT_COMMAND", True) and settings.get(
|
||||
"SHOW_ALERT_COMMAND_IN_MENU", True):
|
||||
menu_text_2 += "alert - show active alerts\n"
|
||||
if settings.get('ENABLE_CUSTOM_LOOKUP', False):
|
||||
if settings.get('SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU', True):
|
||||
menu_text_2 += "loc lat/lon - custom location lookup\n"
|
||||
#check if both show_alert and loc command are disabled
|
||||
if not settings.get('SHOW_ALERT_COMMAND_IN_MENU', True) and not settings.get(
|
||||
'SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU',
|
||||
False):
|
||||
combined_menu = f"{menu_text_1}\n{menu_text_2}".strip()
|
||||
# If both commands are disabled send menu without using split_message
|
||||
interface.sendText(combined_menu,wantAck=True, destinationId=sender_id)
|
||||
return
|
||||
|
||||
if settings.get('FULL_MENU', True):
|
||||
combined_menu = menu_text_1 + "\n" + menu_text_2
|
||||
messages = split_message(combined_menu, message_type="Menu")
|
||||
@ -350,10 +462,10 @@ def message_listener(packet, interface):
|
||||
"4day - 4 day forecast\n" \
|
||||
"temp - 24h temperature\n" \
|
||||
"rain - 24h precipitation"
|
||||
if settings.get('ENABLE_FULL_ALERT_COMMAND', True):
|
||||
if settings.get('ENABLE_ALERT_COMMAND', True):
|
||||
simple_menu += "\nalert - show active alerts"
|
||||
if settings.get('ENABLE_CUSTOM_LOOKUP', False):
|
||||
simple_menu += "loc lat/lon - custom location lookup"
|
||||
simple_menu += "\nloc lat/lon - custom location lookup"
|
||||
messages = split_message(simple_menu, message_type="Menu")
|
||||
send_message_sequence(messages, message_type="Menu")
|
||||
elif "loc" in message:
|
||||
@ -447,7 +559,7 @@ def message_listener(packet, interface):
|
||||
if alerts:
|
||||
if not alerts.broadcast_full_alert(sender_id):
|
||||
time.sleep(first_message_delay)
|
||||
if not settings.get('ENABLE_FULL_ALERT_COMMAND', True):
|
||||
if not settings.get('ENABLE_ALERT_COMMAND', True):
|
||||
messages = split_message(
|
||||
"The full-alert command is disabled in settings.", message_type="Alert"
|
||||
)
|
||||
@ -477,7 +589,7 @@ def message_listener(packet, interface):
|
||||
def signal_handler(sig, frame):
|
||||
"""Perform a graceful shutdown when CTRL+C is pressed"""
|
||||
global interface
|
||||
logger.info("\nInitiating shutdown...")
|
||||
logger.info("\nClosing program. Please wait...")
|
||||
try:
|
||||
if interface is not None:
|
||||
if settings.get('SHUTDOWN_NODE_ON_EXIT', False):
|
||||
@ -492,7 +604,7 @@ def signal_handler(sig, frame):
|
||||
logger.error(f"Error sending shutdown command: {e}")
|
||||
|
||||
else:
|
||||
logger.info("Node shutdown disabled in settings, skipping shutdown command")
|
||||
logger.info("Node shutdown disabled in settings, skipping sending power off command.")
|
||||
|
||||
logger.info("Closing Meshtastic interface...")
|
||||
interface.close()
|
||||
@ -536,7 +648,7 @@ def main():
|
||||
logger.info("No serial ports found.")
|
||||
exit(0)
|
||||
|
||||
logger.info(f"Press CTRL-C to terminate the program")
|
||||
logger.info(f"Press CTRL-C to close the program")
|
||||
|
||||
# Create interface
|
||||
if args.host:
|
||||
|
||||
@ -81,7 +81,7 @@ class WeatherAlerts:
|
||||
def broadcast_full_alert(self, destination_id):
|
||||
"""Broadcast the full alert information including description."""
|
||||
# Check if full-alert command is enabled
|
||||
if not self.settings.get('ENABLE_FULL_ALERT_COMMAND', True):
|
||||
if not self.settings.get('ENABLE_ALERT_COMMAND', True):
|
||||
return False # Do nothing if full-alert command is disabled
|
||||
|
||||
# Check if there's a current alert
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
MYNODES:
|
||||
- "1234567890" #these are examples, fill in with your node numbers if needed, you can add lines as needed.
|
||||
- "1234567890"
|
||||
FIREWALL: false # If true, only responds to node ids listed under "MYNODES:"
|
||||
FIREWALL: false # If true, only responds to node ids listed under "MYNODES"
|
||||
DM_MODE: true # If true, bot responds to direct messages only. Recommend not changing this
|
||||
DUTYCYCLE: false # If true, will limit to 10% duty cycle
|
||||
NWS_OFFICE: "BGM" # Location settings for the weather forecast api calls, see readme for details
|
||||
NWS_GRID_X: "84"
|
||||
NWS_GRID_Y: "89"
|
||||
ALERT_LAT: "37.7654" # Location settings for alerts, use your coordinates. No more than 4 digits past the decimal point
|
||||
ALERT_LAT: "37.7654" # Primary location settings for alerts and forecast. No more than 4 digits past the decimal point
|
||||
ALERT_LON: "-100.0151"
|
||||
NWS_OFFICE: "" #Advance setup options, leave blank unless needed. See readme for details.
|
||||
NWS_GRID_X: ""
|
||||
NWS_GRID_Y: ""
|
||||
ALERT_CHECK_INTERVAL: 300 # Time in seconds between alert checks (default: 300 = 5 minutes)
|
||||
ALERT_INCLUDE_DESCRIPTION: false # Set to false to exclude the full description from automatically issued alerts
|
||||
ALERT_CHANNEL_INDEX: 0 # Channel index for weather alerts, default is 0 (first channel)
|
||||
FIRST_MESSAGE_DELAY: 0 # Delay in seconds between receiving a request and sending the first message back.
|
||||
MESSAGE_DELAY: 15 # Delay in seconds between subsequent messages of a multi-message response
|
||||
ENABLE_FULL_ALERT_COMMAND: true # Set to false to disable the alert request command
|
||||
ENABLE_CUSTOM_LOOKUP: true # Enable/disable custom lat/lon lookup via message
|
||||
ENABLE_ALERT_COMMAND: true # Set to false to disable the alert request command, automatic alerts will not be affected.
|
||||
SHOW_ALERT_COMMAND_IN_MENU: false # When false, hides the command from the menu but keeps it enabled, if enabled.
|
||||
SHOW_CUSTOM_LOOKUP_COMMAND_IN_MENU: true # When false, hides the command from the menu, but it is always enabled
|
||||
ENABLE_7DAY_FORECAST: true # Set to false to disable 7-day forecast module
|
||||
ENABLE_5DAY_FORECAST: true # Set to false to disable 5-day forecast module
|
||||
ENABLE_HOURLY_WEATHER: true # Set to false to disable hourly weather module
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user