weatherlink-bridge 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weatherlink_bridge-0.1.0/PKG-INFO +243 -0
- weatherlink_bridge-0.1.0/README.md +227 -0
- weatherlink_bridge-0.1.0/pyproject.toml +132 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/__init__.py +5 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/__main__.py +5 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/collectors/__init__.py +1 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/collectors/base.py +4 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/collectors/weatherlink.py +215 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/exceptions.py +39 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/logger.py +60 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/main.py +385 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/mapping/__init__.py +1 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/mapping/mapper.py +101 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/mapping/transforms.py +70 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/metrics.py +150 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/models/__init__.py +27 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/models/observation.py +56 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/models/sensor_map.py +105 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/models/weatherlink.py +90 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/__init__.py +8 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/base.py +58 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/factory.py +123 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/windy.py +181 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/wunderground.py +125 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/py.typed +0 -0
- weatherlink_bridge-0.1.0/src/weatherlink_bridge/settings.py +90 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: weatherlink-bridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WeatherLink PWS bridge service forwarding to Weather Underground and Windy
|
|
5
|
+
Author: John Roden
|
|
6
|
+
Author-email: John Roden <rodenj@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: pydantic>=2.7
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.3
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: prometheus-client>=0.20
|
|
12
|
+
Requires-Dist: pyyaml>=6.0
|
|
13
|
+
Requires-Dist: structlog>=24.1
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# WeatherLink Bridge
|
|
18
|
+
|
|
19
|
+
A Python service that polls the **Davis WeatherLink v2 API** and forwards personal weather station observations to **Weather Underground** and **Windy**. It is a clean rewrite of the original Node.js app, adding robust configuration management, Prometheus metrics, and a production-ready container image.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
WeatherLink API
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
WeatherLinkCollector (httpx, X-Api-Secret header)
|
|
30
|
+
|
|
|
31
|
+
v
|
|
32
|
+
WeatherObservation (canonical imperial fields, Pydantic v2)
|
|
33
|
+
|
|
|
34
|
+
+----> FieldMapper(wunderground.yaml) --> WundergroundPublisher --> WU HTTPS API
|
|
35
|
+
|
|
|
36
|
+
+----> FieldMapper(windy.yaml) --> WindyPublisher --> Windy v2 API
|
|
37
|
+
(unit transforms: °F→°C, mph→m/s, inHg→Pa, in→mm)
|
|
38
|
+
|
|
39
|
+
Prometheus /metrics on METRICS_PORT (default 8080)
|
|
40
|
+
- wl_fetch_total, publish_total, collection_run_total
|
|
41
|
+
- observation_value (per-field gauge)
|
|
42
|
+
- last_successful_cycle_timestamp <-- liveness signal
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Publishers are registered via `PublisherFactory` — adding a new destination (CWOP, PWSWeather, …) requires only a new YAML sensor map and a publisher class.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
**Prerequisites:** Python 3.12+, [uv](https://docs.astral.sh/uv/).
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Clone and install
|
|
55
|
+
git clone https://github.com/rodenj1/weatherlink-bridge.git
|
|
56
|
+
cd weatherlink-bridge
|
|
57
|
+
|
|
58
|
+
# Copy the example env file and fill in your credentials
|
|
59
|
+
cp .env.example .env
|
|
60
|
+
$EDITOR .env
|
|
61
|
+
|
|
62
|
+
# Run directly (reads .env automatically)
|
|
63
|
+
uv run weatherlink-bridge
|
|
64
|
+
|
|
65
|
+
# Or install into a venv and run the console script
|
|
66
|
+
uv sync
|
|
67
|
+
weatherlink-bridge --version
|
|
68
|
+
weatherlink-bridge
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Configuration Reference
|
|
74
|
+
|
|
75
|
+
All configuration is via environment variables (or a `.env` file in the working directory). Nested settings use the `__` delimiter.
|
|
76
|
+
|
|
77
|
+
| Variable | Required | Default | Description |
|
|
78
|
+
|---|---|---|---|
|
|
79
|
+
| `WEATHERLINK__API_KEY` | Yes | — | WeatherLink v2 API key |
|
|
80
|
+
| `WEATHERLINK__API_SECRET` | Yes | — | WeatherLink v2 API secret |
|
|
81
|
+
| `WEATHERLINK__STATION_ID` | Yes | — | WeatherLink station ID |
|
|
82
|
+
| `WUNDERGROUND__ENABLED` | No | `false` | Enable Weather Underground publishing |
|
|
83
|
+
| `WUNDERGROUND__STATION_ID` | No | `""` | WU PWS station ID (e.g. `KCASANDI123`) |
|
|
84
|
+
| `WUNDERGROUND__PASSWORD` | No | `""` | WU station key / password |
|
|
85
|
+
| `WINDY__ENABLED` | No | `false` | Enable Windy publishing |
|
|
86
|
+
| `WINDY__STATION_ID` | No | `""` | Windy station ID (numeric) |
|
|
87
|
+
| `WINDY__PASSWORD` | No | `""` | Windy station password (see note below) |
|
|
88
|
+
| `UPDATE_INTERVAL_MINS` | No | `5` | Poll interval in minutes (minimum 5) |
|
|
89
|
+
| `METRICS_PORT` | No | `8080` | TCP port for Prometheus `/metrics` |
|
|
90
|
+
| `LOG_LEVEL` | No | `INFO` | Log verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
|
|
91
|
+
|
|
92
|
+
### Windy credentials note
|
|
93
|
+
|
|
94
|
+
Windy uses a **per-station password** for uploads, not the management API key.
|
|
95
|
+
Find it at [stations.windy.com](https://stations.windy.com/) under
|
|
96
|
+
**My Stations → Station settings → Key/Password**. Set this value in
|
|
97
|
+
`WINDY__PASSWORD`.
|
|
98
|
+
|
|
99
|
+
### Weather Underground credentials note
|
|
100
|
+
|
|
101
|
+
WU needs the **station ID** (`WUNDERGROUND__STATION_ID`, e.g. `KCASANDI123`)
|
|
102
|
+
and the **station key / password** (`WUNDERGROUND__PASSWORD`). Both are found
|
|
103
|
+
in the WU device management dashboard.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Running with Docker
|
|
108
|
+
|
|
109
|
+
### Single container
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Pull the latest image
|
|
113
|
+
docker pull ghcr.io/rodenj1/weatherlink-bridge:latest
|
|
114
|
+
|
|
115
|
+
# Run with a .env file
|
|
116
|
+
docker run --rm --env-file .env -p 8080:8080 ghcr.io/rodenj1/weatherlink-bridge:latest
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### docker-compose
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cp .env.example .env
|
|
123
|
+
$EDITOR .env # fill in real credentials
|
|
124
|
+
docker compose up -d
|
|
125
|
+
# metrics available at http://localhost:8080/metrics
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The image is published to `ghcr.io/rodenj1/weatherlink-bridge` for both
|
|
129
|
+
`linux/amd64` and `linux/arm64` (suitable for Raspberry Pi / ARM homelab).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Running on Kubernetes
|
|
134
|
+
|
|
135
|
+
Manifests are under `deploy/k8s/`.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# 1. Create the namespace
|
|
139
|
+
kubectl create namespace weather
|
|
140
|
+
|
|
141
|
+
# 2. Copy the secret example, fill in real values, apply
|
|
142
|
+
cp deploy/k8s/secret.example.yaml deploy/k8s/secret.yaml
|
|
143
|
+
$EDITOR deploy/k8s/secret.yaml # add real credentials
|
|
144
|
+
kubectl apply -f deploy/k8s/secret.yaml
|
|
145
|
+
|
|
146
|
+
# 3. Apply the rest
|
|
147
|
+
kubectl apply -f deploy/k8s/deployment.yaml
|
|
148
|
+
kubectl apply -f deploy/k8s/service.yaml
|
|
149
|
+
|
|
150
|
+
# Verify
|
|
151
|
+
kubectl -n weather get pods
|
|
152
|
+
kubectl -n weather logs -f deployment/weatherlink-bridge
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> **Do not commit `secret.yaml` with real values.** Use
|
|
156
|
+
> [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) or a
|
|
157
|
+
> secrets manager to encrypt before committing.
|
|
158
|
+
|
|
159
|
+
The Deployment is intentionally `replicas: 1` — two replicas would
|
|
160
|
+
**double-upload** every observation to WU and Windy.
|
|
161
|
+
|
|
162
|
+
### Liveness and readiness probes
|
|
163
|
+
|
|
164
|
+
Both probes hit `GET /metrics` on port 8080. The liveness probe also lets you
|
|
165
|
+
monitor `last_successful_cycle_timestamp` in Prometheus — if that gauge is
|
|
166
|
+
older than `2 * update_interval_seconds`, the service may be stalled.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Metrics
|
|
171
|
+
|
|
172
|
+
The service exposes Prometheus metrics at `http://<host>:8080/metrics`.
|
|
173
|
+
|
|
174
|
+
| Metric | Type | Description |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| `wl_fetch_total` | Counter | WeatherLink fetch attempts (`status=success\|error`) |
|
|
177
|
+
| `wl_fetch_duration_seconds` | Histogram | WeatherLink API latency |
|
|
178
|
+
| `publish_total` | Counter | Publisher attempts (`publisher=wu\|windy`, `status=success\|failure\|skipped`) |
|
|
179
|
+
| `publish_duration_seconds` | Histogram | Publisher call latency |
|
|
180
|
+
| `collection_run_total` | Counter | Full cycle outcomes (`status=success\|partial\|error`) |
|
|
181
|
+
| `collection_run_duration_seconds` | Histogram | Full cycle duration |
|
|
182
|
+
| `observation_value` | Gauge | Latest numeric field value per field + station |
|
|
183
|
+
| `last_successful_cycle_timestamp` | Gauge | Unix timestamp of last successful fetch (liveness signal) |
|
|
184
|
+
| `update_interval_seconds` | Gauge | Configured poll interval |
|
|
185
|
+
| `weatherlink_bridge_info` | Info | App version |
|
|
186
|
+
|
|
187
|
+
### Liveness vs freshness
|
|
188
|
+
|
|
189
|
+
- **Liveness** (`last_successful_cycle_timestamp`): advances after every
|
|
190
|
+
successful WeatherLink fetch, regardless of publisher outcomes. Alert if
|
|
191
|
+
`time() - last_successful_cycle_timestamp > 2 * update_interval_seconds`.
|
|
192
|
+
- **Publisher health**: monitor `publish_total{status="failure"}` separately.
|
|
193
|
+
Publisher failures do not gate liveness — a rate-limited or temporarily
|
|
194
|
+
unavailable WU/Windy should not restart the pod.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Migration from the Node.js version
|
|
199
|
+
|
|
200
|
+
| Old environment variable | New environment variable | Notes |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `WEATHERLINK_API_KEY` | `WEATHERLINK__API_KEY` | Delimiter changed (`_` → `__`) |
|
|
203
|
+
| `WEATHERLINK_API_SECRECT` | `WEATHERLINK__API_SECRET` | **Typo fixed** (`SECRECT` → `SECRET`) |
|
|
204
|
+
| `WEATHERLINK_STATION_ID` | `WEATHERLINK__STATION_ID` | Delimiter changed |
|
|
205
|
+
| `WUNDERGROUND_ID` | `WUNDERGROUND__STATION_ID` | Renamed for clarity |
|
|
206
|
+
| `WUNDERGROUND_KEY` | `WUNDERGROUND__PASSWORD` | Renamed; this is the station password |
|
|
207
|
+
| `UPDATE_INTERVAL_MINS` | `UPDATE_INTERVAL_MINS` | Unchanged |
|
|
208
|
+
| `PORT` | `METRICS_PORT` | Renamed; default is now 8080 |
|
|
209
|
+
|
|
210
|
+
**Sensor map:** `sensor_map.json` is replaced by YAML files in
|
|
211
|
+
`config/sensor_maps/` (`wunderground.yaml`, `windy.yaml`). The YAML format
|
|
212
|
+
supports field transforms (unit conversions) and is checked into version
|
|
213
|
+
control.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Development
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Install all deps including dev tools
|
|
221
|
+
uv sync
|
|
222
|
+
|
|
223
|
+
# Run tests with coverage
|
|
224
|
+
uv run pytest
|
|
225
|
+
|
|
226
|
+
# Lint + format check
|
|
227
|
+
uv run ruff check src tests
|
|
228
|
+
uv run ruff format --check src tests
|
|
229
|
+
|
|
230
|
+
# Type-check
|
|
231
|
+
uv run pyright src
|
|
232
|
+
uv run mypy src
|
|
233
|
+
|
|
234
|
+
# Pre-commit hooks (runs on every commit)
|
|
235
|
+
uv run pre-commit install
|
|
236
|
+
uv run pre-commit run --all-files
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# WeatherLink Bridge
|
|
2
|
+
|
|
3
|
+
A Python service that polls the **Davis WeatherLink v2 API** and forwards personal weather station observations to **Weather Underground** and **Windy**. It is a clean rewrite of the original Node.js app, adding robust configuration management, Prometheus metrics, and a production-ready container image.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
WeatherLink API
|
|
11
|
+
|
|
|
12
|
+
v
|
|
13
|
+
WeatherLinkCollector (httpx, X-Api-Secret header)
|
|
14
|
+
|
|
|
15
|
+
v
|
|
16
|
+
WeatherObservation (canonical imperial fields, Pydantic v2)
|
|
17
|
+
|
|
|
18
|
+
+----> FieldMapper(wunderground.yaml) --> WundergroundPublisher --> WU HTTPS API
|
|
19
|
+
|
|
|
20
|
+
+----> FieldMapper(windy.yaml) --> WindyPublisher --> Windy v2 API
|
|
21
|
+
(unit transforms: °F→°C, mph→m/s, inHg→Pa, in→mm)
|
|
22
|
+
|
|
23
|
+
Prometheus /metrics on METRICS_PORT (default 8080)
|
|
24
|
+
- wl_fetch_total, publish_total, collection_run_total
|
|
25
|
+
- observation_value (per-field gauge)
|
|
26
|
+
- last_successful_cycle_timestamp <-- liveness signal
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Publishers are registered via `PublisherFactory` — adding a new destination (CWOP, PWSWeather, …) requires only a new YAML sensor map and a publisher class.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
**Prerequisites:** Python 3.12+, [uv](https://docs.astral.sh/uv/).
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Clone and install
|
|
39
|
+
git clone https://github.com/rodenj1/weatherlink-bridge.git
|
|
40
|
+
cd weatherlink-bridge
|
|
41
|
+
|
|
42
|
+
# Copy the example env file and fill in your credentials
|
|
43
|
+
cp .env.example .env
|
|
44
|
+
$EDITOR .env
|
|
45
|
+
|
|
46
|
+
# Run directly (reads .env automatically)
|
|
47
|
+
uv run weatherlink-bridge
|
|
48
|
+
|
|
49
|
+
# Or install into a venv and run the console script
|
|
50
|
+
uv sync
|
|
51
|
+
weatherlink-bridge --version
|
|
52
|
+
weatherlink-bridge
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Configuration Reference
|
|
58
|
+
|
|
59
|
+
All configuration is via environment variables (or a `.env` file in the working directory). Nested settings use the `__` delimiter.
|
|
60
|
+
|
|
61
|
+
| Variable | Required | Default | Description |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `WEATHERLINK__API_KEY` | Yes | — | WeatherLink v2 API key |
|
|
64
|
+
| `WEATHERLINK__API_SECRET` | Yes | — | WeatherLink v2 API secret |
|
|
65
|
+
| `WEATHERLINK__STATION_ID` | Yes | — | WeatherLink station ID |
|
|
66
|
+
| `WUNDERGROUND__ENABLED` | No | `false` | Enable Weather Underground publishing |
|
|
67
|
+
| `WUNDERGROUND__STATION_ID` | No | `""` | WU PWS station ID (e.g. `KCASANDI123`) |
|
|
68
|
+
| `WUNDERGROUND__PASSWORD` | No | `""` | WU station key / password |
|
|
69
|
+
| `WINDY__ENABLED` | No | `false` | Enable Windy publishing |
|
|
70
|
+
| `WINDY__STATION_ID` | No | `""` | Windy station ID (numeric) |
|
|
71
|
+
| `WINDY__PASSWORD` | No | `""` | Windy station password (see note below) |
|
|
72
|
+
| `UPDATE_INTERVAL_MINS` | No | `5` | Poll interval in minutes (minimum 5) |
|
|
73
|
+
| `METRICS_PORT` | No | `8080` | TCP port for Prometheus `/metrics` |
|
|
74
|
+
| `LOG_LEVEL` | No | `INFO` | Log verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
|
|
75
|
+
|
|
76
|
+
### Windy credentials note
|
|
77
|
+
|
|
78
|
+
Windy uses a **per-station password** for uploads, not the management API key.
|
|
79
|
+
Find it at [stations.windy.com](https://stations.windy.com/) under
|
|
80
|
+
**My Stations → Station settings → Key/Password**. Set this value in
|
|
81
|
+
`WINDY__PASSWORD`.
|
|
82
|
+
|
|
83
|
+
### Weather Underground credentials note
|
|
84
|
+
|
|
85
|
+
WU needs the **station ID** (`WUNDERGROUND__STATION_ID`, e.g. `KCASANDI123`)
|
|
86
|
+
and the **station key / password** (`WUNDERGROUND__PASSWORD`). Both are found
|
|
87
|
+
in the WU device management dashboard.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Running with Docker
|
|
92
|
+
|
|
93
|
+
### Single container
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Pull the latest image
|
|
97
|
+
docker pull ghcr.io/rodenj1/weatherlink-bridge:latest
|
|
98
|
+
|
|
99
|
+
# Run with a .env file
|
|
100
|
+
docker run --rm --env-file .env -p 8080:8080 ghcr.io/rodenj1/weatherlink-bridge:latest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### docker-compose
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cp .env.example .env
|
|
107
|
+
$EDITOR .env # fill in real credentials
|
|
108
|
+
docker compose up -d
|
|
109
|
+
# metrics available at http://localhost:8080/metrics
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The image is published to `ghcr.io/rodenj1/weatherlink-bridge` for both
|
|
113
|
+
`linux/amd64` and `linux/arm64` (suitable for Raspberry Pi / ARM homelab).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Running on Kubernetes
|
|
118
|
+
|
|
119
|
+
Manifests are under `deploy/k8s/`.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# 1. Create the namespace
|
|
123
|
+
kubectl create namespace weather
|
|
124
|
+
|
|
125
|
+
# 2. Copy the secret example, fill in real values, apply
|
|
126
|
+
cp deploy/k8s/secret.example.yaml deploy/k8s/secret.yaml
|
|
127
|
+
$EDITOR deploy/k8s/secret.yaml # add real credentials
|
|
128
|
+
kubectl apply -f deploy/k8s/secret.yaml
|
|
129
|
+
|
|
130
|
+
# 3. Apply the rest
|
|
131
|
+
kubectl apply -f deploy/k8s/deployment.yaml
|
|
132
|
+
kubectl apply -f deploy/k8s/service.yaml
|
|
133
|
+
|
|
134
|
+
# Verify
|
|
135
|
+
kubectl -n weather get pods
|
|
136
|
+
kubectl -n weather logs -f deployment/weatherlink-bridge
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
> **Do not commit `secret.yaml` with real values.** Use
|
|
140
|
+
> [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) or a
|
|
141
|
+
> secrets manager to encrypt before committing.
|
|
142
|
+
|
|
143
|
+
The Deployment is intentionally `replicas: 1` — two replicas would
|
|
144
|
+
**double-upload** every observation to WU and Windy.
|
|
145
|
+
|
|
146
|
+
### Liveness and readiness probes
|
|
147
|
+
|
|
148
|
+
Both probes hit `GET /metrics` on port 8080. The liveness probe also lets you
|
|
149
|
+
monitor `last_successful_cycle_timestamp` in Prometheus — if that gauge is
|
|
150
|
+
older than `2 * update_interval_seconds`, the service may be stalled.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Metrics
|
|
155
|
+
|
|
156
|
+
The service exposes Prometheus metrics at `http://<host>:8080/metrics`.
|
|
157
|
+
|
|
158
|
+
| Metric | Type | Description |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `wl_fetch_total` | Counter | WeatherLink fetch attempts (`status=success\|error`) |
|
|
161
|
+
| `wl_fetch_duration_seconds` | Histogram | WeatherLink API latency |
|
|
162
|
+
| `publish_total` | Counter | Publisher attempts (`publisher=wu\|windy`, `status=success\|failure\|skipped`) |
|
|
163
|
+
| `publish_duration_seconds` | Histogram | Publisher call latency |
|
|
164
|
+
| `collection_run_total` | Counter | Full cycle outcomes (`status=success\|partial\|error`) |
|
|
165
|
+
| `collection_run_duration_seconds` | Histogram | Full cycle duration |
|
|
166
|
+
| `observation_value` | Gauge | Latest numeric field value per field + station |
|
|
167
|
+
| `last_successful_cycle_timestamp` | Gauge | Unix timestamp of last successful fetch (liveness signal) |
|
|
168
|
+
| `update_interval_seconds` | Gauge | Configured poll interval |
|
|
169
|
+
| `weatherlink_bridge_info` | Info | App version |
|
|
170
|
+
|
|
171
|
+
### Liveness vs freshness
|
|
172
|
+
|
|
173
|
+
- **Liveness** (`last_successful_cycle_timestamp`): advances after every
|
|
174
|
+
successful WeatherLink fetch, regardless of publisher outcomes. Alert if
|
|
175
|
+
`time() - last_successful_cycle_timestamp > 2 * update_interval_seconds`.
|
|
176
|
+
- **Publisher health**: monitor `publish_total{status="failure"}` separately.
|
|
177
|
+
Publisher failures do not gate liveness — a rate-limited or temporarily
|
|
178
|
+
unavailable WU/Windy should not restart the pod.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Migration from the Node.js version
|
|
183
|
+
|
|
184
|
+
| Old environment variable | New environment variable | Notes |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `WEATHERLINK_API_KEY` | `WEATHERLINK__API_KEY` | Delimiter changed (`_` → `__`) |
|
|
187
|
+
| `WEATHERLINK_API_SECRECT` | `WEATHERLINK__API_SECRET` | **Typo fixed** (`SECRECT` → `SECRET`) |
|
|
188
|
+
| `WEATHERLINK_STATION_ID` | `WEATHERLINK__STATION_ID` | Delimiter changed |
|
|
189
|
+
| `WUNDERGROUND_ID` | `WUNDERGROUND__STATION_ID` | Renamed for clarity |
|
|
190
|
+
| `WUNDERGROUND_KEY` | `WUNDERGROUND__PASSWORD` | Renamed; this is the station password |
|
|
191
|
+
| `UPDATE_INTERVAL_MINS` | `UPDATE_INTERVAL_MINS` | Unchanged |
|
|
192
|
+
| `PORT` | `METRICS_PORT` | Renamed; default is now 8080 |
|
|
193
|
+
|
|
194
|
+
**Sensor map:** `sensor_map.json` is replaced by YAML files in
|
|
195
|
+
`config/sensor_maps/` (`wunderground.yaml`, `windy.yaml`). The YAML format
|
|
196
|
+
supports field transforms (unit conversions) and is checked into version
|
|
197
|
+
control.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Install all deps including dev tools
|
|
205
|
+
uv sync
|
|
206
|
+
|
|
207
|
+
# Run tests with coverage
|
|
208
|
+
uv run pytest
|
|
209
|
+
|
|
210
|
+
# Lint + format check
|
|
211
|
+
uv run ruff check src tests
|
|
212
|
+
uv run ruff format --check src tests
|
|
213
|
+
|
|
214
|
+
# Type-check
|
|
215
|
+
uv run pyright src
|
|
216
|
+
uv run mypy src
|
|
217
|
+
|
|
218
|
+
# Pre-commit hooks (runs on every commit)
|
|
219
|
+
uv run pre-commit install
|
|
220
|
+
uv run pre-commit run --all-files
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.7,<0.12.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "weatherlink-bridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "WeatherLink PWS bridge service forwarding to Weather Underground and Windy"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "John Roden", email = "rodenj@gmail.com" }]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"pydantic>=2.7",
|
|
15
|
+
"pydantic-settings>=2.3",
|
|
16
|
+
"httpx>=0.27",
|
|
17
|
+
"prometheus-client>=0.20",
|
|
18
|
+
"pyyaml>=6.0",
|
|
19
|
+
"structlog>=24.1",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
weatherlink-bridge = "weatherlink_bridge.main:main"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=8",
|
|
28
|
+
"pytest-asyncio>=1",
|
|
29
|
+
"pytest-cov>=5",
|
|
30
|
+
"respx>=0.21",
|
|
31
|
+
"ruff>=0.6",
|
|
32
|
+
"mypy>=1.10",
|
|
33
|
+
"pyright>=1.1.400",
|
|
34
|
+
"pre-commit>=3",
|
|
35
|
+
"commitizen>=4",
|
|
36
|
+
"types-PyYAML",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
addopts = [
|
|
41
|
+
"--cov=src/weatherlink_bridge",
|
|
42
|
+
"--cov-report=term-missing",
|
|
43
|
+
"--cov-report=html:htmlcov",
|
|
44
|
+
"--cov-fail-under=95",
|
|
45
|
+
"--strict-markers",
|
|
46
|
+
"--strict-config",
|
|
47
|
+
]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
norecursedirs = ["manual"]
|
|
50
|
+
asyncio_mode = "auto"
|
|
51
|
+
markers = [
|
|
52
|
+
"integration: wired multi-component tests",
|
|
53
|
+
"live: hits the real WeatherLink API; skipped unless WLB_LIVE_TESTS=1",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.coverage.run]
|
|
57
|
+
source = ["src"]
|
|
58
|
+
branch = true
|
|
59
|
+
omit = ["*/tests/*", "*/test_*", "conftest.py"]
|
|
60
|
+
|
|
61
|
+
[tool.coverage.report]
|
|
62
|
+
exclude_lines = [
|
|
63
|
+
"pragma: no cover",
|
|
64
|
+
"def __repr__",
|
|
65
|
+
"raise AssertionError",
|
|
66
|
+
"raise NotImplementedError",
|
|
67
|
+
"if __name__ == .__main__.:",
|
|
68
|
+
"if TYPE_CHECKING:",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.mypy]
|
|
72
|
+
python_version = "3.12"
|
|
73
|
+
strict = true
|
|
74
|
+
warn_return_any = true
|
|
75
|
+
warn_unused_configs = true
|
|
76
|
+
disallow_untyped_defs = true
|
|
77
|
+
disallow_incomplete_defs = true
|
|
78
|
+
check_untyped_defs = true
|
|
79
|
+
disallow_untyped_decorators = true
|
|
80
|
+
no_implicit_optional = true
|
|
81
|
+
warn_redundant_casts = true
|
|
82
|
+
warn_unused_ignores = true
|
|
83
|
+
warn_no_return = true
|
|
84
|
+
warn_unreachable = true
|
|
85
|
+
strict_equality = true
|
|
86
|
+
|
|
87
|
+
[[tool.mypy.overrides]]
|
|
88
|
+
module = "prometheus_client.*"
|
|
89
|
+
ignore_missing_imports = true
|
|
90
|
+
|
|
91
|
+
[tool.ruff]
|
|
92
|
+
line-length = 88
|
|
93
|
+
target-version = "py312"
|
|
94
|
+
extend-exclude = ["docs", "htmlcov", "build", "dist", ".venv"]
|
|
95
|
+
|
|
96
|
+
[tool.ruff.lint]
|
|
97
|
+
select = ["E", "W", "F", "I", "B", "UP", "N", "SIM", "RUF"]
|
|
98
|
+
ignore = ["E501"]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.per-file-ignores]
|
|
101
|
+
"tests/**/*.py" = ["B", "N", "SIM"]
|
|
102
|
+
"conftest.py" = ["B", "N"]
|
|
103
|
+
|
|
104
|
+
[tool.ruff.format]
|
|
105
|
+
quote-style = "double"
|
|
106
|
+
indent-style = "space"
|
|
107
|
+
line-ending = "auto"
|
|
108
|
+
|
|
109
|
+
[tool.pyright]
|
|
110
|
+
include = ["src"]
|
|
111
|
+
typeCheckingMode = "strict"
|
|
112
|
+
deprecateTypingAliases = true
|
|
113
|
+
reportDeprecated = "warning"
|
|
114
|
+
extraPaths = ["src/"]
|
|
115
|
+
|
|
116
|
+
[tool.commitizen]
|
|
117
|
+
name = "cz_customize"
|
|
118
|
+
version = "0.1.0"
|
|
119
|
+
version_provider = "pep621"
|
|
120
|
+
version_files = ["src/weatherlink_bridge/__init__.py:__version__"]
|
|
121
|
+
tag_format = "v$version"
|
|
122
|
+
update_changelog_on_bump = true
|
|
123
|
+
major_version_zero = true
|
|
124
|
+
allowed_prefixes = ["Merge", "Revert", "Pull request", "fixup!", "squash!"]
|
|
125
|
+
bump_message = "release: bump $current_version -> $new_version"
|
|
126
|
+
|
|
127
|
+
[tool.commitizen.customize]
|
|
128
|
+
schema_pattern = "^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|release|security)(\\([a-z0-9_\\-.,/]+\\))?!?: .+"
|
|
129
|
+
bump_pattern = "^(feat|fix|refactor|perf|security|build)"
|
|
130
|
+
bump_map = { "^feat" = "MINOR", "^fix" = "PATCH", "^refactor" = "PATCH", "^perf" = "PATCH", "^security" = "PATCH", "^build" = "PATCH" }
|
|
131
|
+
change_type_order = ["security", "feat", "fix", "refactor", "perf", "build", "ci", "docs", "style", "test", "chore"]
|
|
132
|
+
info = "WeatherLink Bridge follows Conventional Commits 1.0.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Weather data collectors."""
|