Skip to content

Commit 52a5905

Browse files
authored
Read MQTT password from file (#11)
* Add a documentation on how to contribute to the project * add a docker compose example * enhance copyright year * new version, description of new variable MQTT_PASSWORD_FILE and more unit test documentation * increase version * fixing ruff command * optimize code validation pipeline * clean up requirements and optimize code test pipeline * make python doc more useable, increase version, implement new feature to read mqtt password from file, clean up code in main block
1 parent fcbd9e2 commit 52a5905

File tree

10 files changed

+143
-81
lines changed

10 files changed

+143
-81
lines changed

.github/workflows/code-validation.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ jobs:
2222
- name: Install dependencies
2323
run: |
2424
python -m pip install --upgrade pip
25-
pip install ruff pytest
2625
pip install -r ./src/requirements.txt
26+
pip install -r ./test/requirements.txt
2727
- name: Lint with ruff
2828
run: |
2929
# stop the build if there are Python syntax errors or undefined names

CONTRIBUTING.md

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,46 @@
1-
https://docs.github.com/articles/setting-guidelines-for-repository-contributors/
2-
# Contributing Guidelines
3-
4-
## Basic guidelines
5-
- Commits needs to have a valid GPG signature
6-
- If using 3rd party components, the licence needs to be compatible with the license of this project
7-
- Pull requests should describe the content of the code
8-
- All pull request checks have to pass successfully
9-
-
1+
## How to contribute to weather2mqtt
2+
3+
#### **Did you find a bug?**
4+
5+
* **Do not open up a GitHub issue if the bug is a security vulnerability
6+
in weather2mqtt**, and instead write me a mail to [info@oberdorf-itc.de](mailto:info@oberdorf-itc.de).
7+
8+
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/cybcon/docker.weather2mqtt/issues).
9+
10+
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/cybcon/docker.weather2mqtt/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
11+
12+
#### **Did you write a patch that fixes a bug?**
13+
14+
* Open a new GitHub pull request with the patch.
15+
16+
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
17+
18+
* All commits needs to have a valid GPG signature
19+
20+
* All pull request checks have to pass successfully
21+
22+
* If using 3rd party components, the licence needs to be compatible with the license of this project
23+
24+
#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
25+
26+
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of the application will generally not be accepted.
27+
28+
#### **Do you intend to add a new feature or change an existing one?**
29+
30+
* Open a new GitHub pull request with the patch.
31+
32+
* Ensure the PR description clearly describes the new feature or the change. Include the relevant issue number if applicable.
33+
34+
* All commits needs to have a valid GPG signature
35+
36+
* All pull request checks have to pass successfully
37+
38+
* If using 3rd party components, the licence needs to be compatible with the license of this project
39+
40+
#### **Do you have questions about the source code?**
41+
42+
* Write me a mail to [info@oberdorf-itc.de](mailto:info@oberdorf-itc.de).
43+
44+
Thanks! :heart: :heart: :heart:
45+
46+
Michael

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM alpine:3.23.2
22

33
LABEL maintainer="Michael Oberdorf <info@oberdorf-itc.de>"
4-
LABEL site.local.program.version="1.0.2"
4+
LABEL site.local.program.version="1.1.0"
55

66
ENV TZ="UTC" \
77
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 Michael Oberdorf IT-Consulting
3+
Copyright (c) 2025-2026 Michael Oberdorf IT-Consulting
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ Container image: [DockerHub](https://hub.docker.com/r/oitc/weather2mqtt)
2626

2727
# Supported tags and respective `Dockerfile` links
2828

29-
* [`latest`, `1.0.2`](https://github.com/cybcon/docker.weather2mqtt/blob/v1.0.2/Dockerfile)
29+
* [`latest`, `1.1.0`](https://github.com/cybcon/docker.weather2mqtt/blob/v1.1.0/Dockerfile)
30+
* [`1.0.2`](https://github.com/cybcon/docker.weather2mqtt/blob/v1.0.2/Dockerfile)
3031
* [`1.0.0`](https://github.com/cybcon/docker.weather2mqtt/blob/v1.0.0/Dockerfile)
3132

3233
# Summary
@@ -129,6 +130,7 @@ The container grab some configuration via environment variables.
129130
| `REQUESTS_CA_BUNDLE` | Path to certificates of trusted certificate authorities. | optional | `/etc/ssl/certs/ca-certificates.crt` |
130131
| `MQTT_USERNAME` | Username to authenticate to MQTT broker. | optional | |
131132
| `MQTT_PASSWORD` | Password to authenticate to MQTT broker. | optional | |
133+
| `MQTT_PASSWORD_FILE` | File that contains the password to authenticate to MQTT broker. | optional | |
132134
| `MQTT_SERVER` | MQTT broker hostname to connect to. | optional | `test.mosquitto.org` |
133135
| `MQTT_PORT` | MQTT broker TCP port to connect to. | optional | `1883` |
134136
| `MQTT_RETAIN` | Publish MQTT message in retain mode fpr persistance. | optional | `false` |
@@ -139,14 +141,15 @@ The container grab some configuration via environment variables.
139141

140142
### .envrc example
141143

144+
If you use `direnv` to load your environment automatically.
145+
142146
```bash
143147
export TZ="Europe/Berlin"
144148
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
145149
export DEBUG="true"
146150
export MODE="current"
147-
export LATITUDE="48.7801"
148-
export LONGITUDE="8.9321"
149-
export ELEVATION="409.0"
151+
export LATITUDE="48.72592"
152+
export LONGITUDE="9.11446"
150153
export WEATHER_MODELS="icon_d2"
151154
export MQTT_SERVER="test.mosquitto.org"
152155
export MQTT_PORT="8883"
@@ -178,12 +181,16 @@ To trigger the Python unit tests please follow following instrructions after che
178181

179182
```bash
180183
pip install -r src/requirements.txt
184+
pip install -r test/requirements.txt
181185
```
182186

183187
### Execute Unit Tests
184188

185189
```bash
186190
python -m unittest
191+
pytest
192+
ruff check --select=E9,F63,F7,F82 --target-version=py312 .
193+
ruff check --target-version=py312 .
187194
```
188195

189196
# Donate
@@ -193,7 +200,7 @@ I would appreciate a small donation to support the further development of my ope
193200

194201
# License
195202

196-
Copyright (c) 2025 Michael Oberdorf IT-Consulting
203+
Copyright (c) 2025-2026 Michael Oberdorf IT-Consulting
197204

198205
Permission is hereby granted, free of charge, to any person obtaining a copy
199206
of this software and associated documentation files (the "Software"), to deal

docker-compose.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
services:
2+
weather2mqtt:
3+
container_name: weater2mqtt
4+
image: oitc/weather2mqtt:latest
5+
restart: "no"
6+
user: 3985:3985
7+
environment:
8+
TZ: Europe/Berlin
9+
DEBUG: true
10+
MODE: current
11+
LATITUDE: 48.72592
12+
LONGITUDE: 9.11446
13+
WEATHER_MODELS: icon_d2
14+
MQTT_SERVER: test.mosquitto.org
15+
MQTT_PORT: 8883
16+
MQTT_TLS: true
17+
MQTT_TLS_INSECURE: false
18+
MQTT_PROTOCOL_VERSION: 5
19+
MQTT_TOPIC: github.com/cybcon/docker.weather2mqtt.git/weather
20+
CACHE_EXPIRY_AFTER_SEC: 10

src/app/bin/weather2mqtt.py

Lines changed: 59 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
"""
22
###############################################################################
3-
# Tool to get weather information from the Open-Meteo API and publish it to MQTT\n
4-
# seeAlso: https://open-meteo.com/en/docs\n
5-
#------------------------------------------------------------------------------\n
6-
# This is a Hackergarden project, started during 50th Hackergarden 2025-04-01\n
7-
# at codecentric AG, Industriestraße 3, 70565 Stuttgart, Germany\n
8-
# seeAlso: https://www.hackergarten.net/\n
9-
# seeAlso: https://www.codecentric.de/standorte/stuttgart\n
10-
#------------------------------------------------------------------------------\n
11-
# Author: Michael Oberdorf\n
12-
# Date: 2025-04-11\n
13-
# Last modified by: Michael Oberdorf\n
14-
# Last modified at: 2025-12-31\n
15-
###############################################################################\n
3+
# Tool to get weather information from the Open-Meteo API and publish it to MQTT
4+
# seeAlso: https://open-meteo.com/en/docs
5+
#------------------------------------------------------------------------------
6+
# This is a Hackergarden project, started during 50th Hackergarden 2025-04-01
7+
# at codecentric AG, Industriestraße 3, 70565 Stuttgart, Germany
8+
# seeAlso: https://www.hackergarten.net/
9+
# seeAlso: https://www.codecentric.de/standorte/stuttgart
10+
#------------------------------------------------------------------------------
11+
# Author: Michael Oberdorf
12+
# Date: 2025-04-11
13+
# Last modified by: Michael Oberdorf
14+
# Last modified at: 2026-01-01
15+
###############################################################################
1616
"""
1717

1818
import datetime
@@ -22,15 +22,14 @@
2222
import ssl
2323
import sys
2424

25-
# import numpy
2625
import openmeteo_requests
2726
import paho.mqtt.client as mqtt
2827
import pytz
2928
import requests_cache # seeAlso: https://pypi.org/project/requests-cache/
3029
from lib.weather_codes import translate_weather_code
3130
from retry_requests import retry # seeAlso: https://pypi.org/project/retry-requests/
3231

33-
__version__ = "1.0.2"
32+
__version__ = "1.1.0"
3433
__script_path__ = os.path.dirname(__file__)
3534
__config_path__ = os.path.join(os.path.dirname(__script_path__), "etc")
3635
__local_tz__ = pytz.timezone("UTC")
@@ -72,12 +71,13 @@ def __makeAbsolutePath(path: str) -> str:
7271

7372
def initialize_logger(severity: int = logging.INFO) -> logging.Logger:
7473
"""
75-
Initialize the logger with the given severity level.\n
76-
:param severity int: The optional severity level for the logger. (default: 20 (INFO))\n
77-
:return logging.RootLogger: The initialized logger.\n
78-
:raise ValueError: If the severity level is not valid.\n
79-
:raise TypeError: If the severity level is not an integer.\n
80-
:raise Exception: If the logger cannot be initialized.\n
74+
Initialize the logger with the given severity level.
75+
76+
:param severity int: The optional severity level for the logger. (default: 20 (INFO))
77+
:return logging.RootLogger: The initialized logger.
78+
:raise ValueError: If the severity level is not valid.
79+
:raise TypeError: If the severity level is not an integer.
80+
:raise Exception: If the logger cannot be initialized.
8181
"""
8282
valid_severity = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
8383
if severity not in valid_severity:
@@ -97,11 +97,12 @@ def initialize_logger(severity: int = logging.INFO) -> logging.Logger:
9797

9898
def load_config_file() -> dict:
9999
"""
100-
Load the configuration file and return the configuration as a dictionary.\n
101-
:return dict: The loaded configuration.\n
102-
:raise ValueError: If the configuration file is not valid.\n
103-
:raise TypeError: If the configuration file is not a string.\n
104-
:raise Exception: If the configuration file cannot be loaded.\n
100+
Load the configuration file and return the configuration as a dictionary.
101+
102+
:return dict: The loaded configuration.
103+
:raise ValueError: If the configuration file is not valid.
104+
:raise TypeError: If the configuration file is not a string.
105+
:raise Exception: If the configuration file cannot be loaded.
105106
"""
106107
if os.environ.get("MODE") is not None:
107108
config_file = os.path.join(__config_path__, os.environ.get("MODE") + ".json")
@@ -134,9 +135,11 @@ def load_config_file() -> dict:
134135

135136
def initialize_mqtt_client() -> mqtt.Client:
136137
"""
137-
Initialize the MQTT client with the given configuration from environment.\n
138-
:return mqtt.Client: The initialized MQTT client.\n
139-
:raise Exception: If the MQTT client cannot be initialized.\n
138+
Initialize the MQTT client with the given configuration from environment.
139+
140+
:return mqtt.Client: The initialized MQTT client.
141+
:raise ValueError: If the MQTT client configuration is not valid.
142+
:raise Exception: If the MQTT client cannot be initialized.
140143
"""
141144
if os.environ.get("MQTT_CLIENT_ID") is not None:
142145
log.debug("Use MQTT client ID: {}".format(os.environ.get("MQTT_CLIENT_ID")))
@@ -185,19 +188,28 @@ def initialize_mqtt_client() -> mqtt.Client:
185188
client.tls_insecure_set(False)
186189

187190
# configure authentication
188-
if os.environ.get("MQTT_USERNAME") is not None and os.environ.get("MQTT_PASSWORD") is not None:
191+
mqtt_pass = None
192+
if os.environ.get("MQTT_PASSWORD") is not None:
193+
mqtt_pass = os.environ.get("MQTT_PASSWORD")
194+
if os.environ.get("MQTT_PASSWORD_FILE") is not None:
195+
if not os.path.isfile(os.environ.get("MQTT_PASSWORD_FILE")):
196+
raise ValueError("MQTT password file {} not found.".format(os.environ.get("MQTT_PASSWORD_FILE")))
197+
with open(os.environ.get("MQTT_PASSWORD_FILE"), "r") as f:
198+
mqtt_pass = f.read().strip()
199+
if os.environ.get("MQTT_USERNAME") is not None and mqtt_pass is not None:
189200
log.debug("Set username ({}) and password for MQTT connection".format(os.environ.get("MQTT_USERNAME")))
190-
client.username_pw_set(os.environ.get("MQTT_USERNAME"), os.environ.get("MQTT_PASSWORD"))
201+
client.username_pw_set(os.environ.get("MQTT_USERNAME"), mqtt_pass)
191202

192203
return client
193204

194205

195206
def request_weather_data(payload: dict) -> dict:
196207
"""
197-
Request weather data from the Open-Meteo API with the given configuration.\n
198-
:param payload dict: The configuration for the Open-Meteo API request.\n
199-
:return dict: The weather data from the Open-Meteo API.\n
200-
:raise Exception: If the weather data cannot be requested.\n
208+
Request weather data from the Open-Meteo API with the given configuration.
209+
210+
:param payload dict: The configuration for the Open-Meteo API request.
211+
:return dict: The weather data from the Open-Meteo API.
212+
:raise Exception: If the weather data cannot be requested.
201213
"""
202214
log.debug(f"Request Open-Meteo API {__open_meteo_api_url__} with parameters: {payload}")
203215
# initialize cache directory
@@ -240,11 +252,12 @@ def request_weather_data(payload: dict) -> dict:
240252

241253
def parse_current_weather(data: any, fields: list = []) -> dict:
242254
"""
243-
Parse the current weather data from the Open-Meteo API response.\n
244-
:param data openmeteo_requests.VariablesWithTime: The Open-Meteo API response from current weather.\n
245-
:param fields list: The list of fields to parse from the current weather data. (default: [])\n
246-
:return dict: The parsed current weather data.\n
247-
:raise Exception: If the current weather data cannot be parsed.\n
255+
Parse the current weather data from the Open-Meteo API response.
256+
257+
:param data openmeteo_requests.VariablesWithTime: The Open-Meteo API response from current weather.
258+
:param fields list: The list of fields to parse from the current weather data. (default: [])
259+
:return dict: The parsed current weather data.
260+
:raise Exception: If the current weather data cannot be parsed.
248261
"""
249262
if not data:
250263
raise Exception("No current weather data found in the response.")
@@ -269,11 +282,12 @@ def parse_current_weather(data: any, fields: list = []) -> dict:
269282

270283
def parse_daily_weather(data: any, fields: list = []) -> dict:
271284
"""
272-
Parse the daily weather data from the Open-Meteo API response.\n
273-
:param data openmeteo_requests.VariablesWithTime: The Open-Meteo API response from current weather.\n
274-
:param fields list: The list of fields to parse from the current weather data. (default: [])\n
275-
:return dict: The parsed current weather data.\n
276-
:raise Exception: If the current weather data cannot be parsed.\n
285+
Parse the daily weather data from the Open-Meteo API response.
286+
287+
:param data openmeteo_requests.VariablesWithTime: The Open-Meteo API response from current weather.
288+
:param fields list: The list of fields to parse from the current weather data. (default: [])
289+
:return dict: The parsed current weather data.
290+
:raise Exception: If the current weather data cannot be parsed.
277291
"""
278292
if not data:
279293
raise Exception("No current weather data found in the response.")
@@ -352,24 +366,6 @@ def parse_daily_weather(data: any, fields: list = []) -> dict:
352366
# Add the local time as message_timestamp to payload
353367
weather_result["message_timestamp"] = __local_tz__.localize(datetime.datetime.now()).isoformat()
354368

355-
"""
356-
PAYLOAD = {
357-
"message_timestamp": __local_tz__.localize(datetime.datetime.now()).isoformat(),
358-
"weather": {
359-
"temperature": 20,
360-
"humidity": 50,
361-
"wind_speed": 5,
362-
"precipitation": 0,
363-
"weather_code": 0,
364-
"weather_code_text": translate_weather_code(0),
365-
},
366-
"location": {
367-
"latitude": float(os.environ.get("LATITUDE")),
368-
"longitude": float(os.environ.get("LONGITUDE")),
369-
"elevation": 409.0,
370-
},
371-
}
372-
"""
373369
log.debug("Payload: {}".format(json.dumps(weather_result)))
374370

375371
# initialize MQTT client and connect to broker

src/requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
openmeteo_requests>=1.3.0,<2
2-
#pandas>=2.2.3,<3
32
paho-mqtt>=2.1.0
43
pytz>=2024.2
54
requests>=2.32.3

test/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616
os.environ["ELEVATION"] = "3.0"
1717
os.environ["WEATHER_MODELS"] = "FooBar"
1818
os.environ["TZ"] = "UTC"
19+
os.environ["MQTT_CLIENT_ID"] = ""

test/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pytest>=9.0.0
2+
ruff>=0.14.0

0 commit comments

Comments
 (0)