tariffkit 0.2.0__tar.gz → 0.2.2__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.
- {tariffkit-0.2.0 → tariffkit-0.2.2}/CHANGELOG.md +56 -1
- {tariffkit-0.2.0 → tariffkit-0.2.2}/PKG-INFO +28 -2
- {tariffkit-0.2.0 → tariffkit-0.2.2}/README.md +26 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/pyproject.toml +6 -2
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/cli.py +7 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/mqtt/publisher.py +29 -1
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_account_cli.py +12 -1
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_mqtt_publisher.py +92 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_private_config.py +4 -1
- {tariffkit-0.2.0 → tariffkit-0.2.2}/.gitignore +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/LICENSE +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/account/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/account/cli.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/account/errors.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/account/model.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/account/rates.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/account/repository.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/billing/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/billing/engine.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/billing/ledger.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/billing/models.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/billing/netting.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/billing/trueup.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/cca.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/config.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/cca/mce/2023-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/cca/mce/2026-04-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/export/pge/acc_plus/2023-04-15.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/export/pge/nbt00.json.gz +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/export/pge/nbt23.json.gz +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/export/pge/nbt24.json.gz +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/export/pge/nbt25.json.gz +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/export/pge/nbt26.json.gz +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/holidays.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/manifest.json +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/nsc/pge.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/eelec/2025-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/eelec/2025-03-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/eelec/2025-09-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/eelec/2026-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/eelec/2026-03-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/etouc/2025-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/etouc/2025-03-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/etouc/2025-09-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/etouc/2026-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/etouc/2026-03-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/ev2a/2025-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/ev2a/2025-03-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/ev2a/2025-09-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/ev2a/2026-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tariff/pge/ev2a/2026-03-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tax/ca_energy_resources/2025-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tax/ca_energy_resources/2026-01-01.toml +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/versioned.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/engine.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/errors.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/export/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/export/nbt.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/interop/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/interop/emhass.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/interop/predbat.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/interop/slots.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/models.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/mqtt/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/mqtt/discovery.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/providers/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/providers/pge/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/providers/pge/reconcile.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/providers/pge/statements/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/providers/pge/statements/errors.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/providers/pge/statements/model.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/providers/pge/statements/ocr.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/providers/pge/statements/parse.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/py.typed +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/secrets.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/sources/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/sources/greenbutton.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/sources/homeassistant.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/sources/influx.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/sources/pge.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/tariff/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/tariff/retail.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/timeutil.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/web/__init__.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/web/app.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/conftest.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/fixtures/nbt_golden_rows.jsonl +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/fixtures/statements/synthetic_cca_ratechange.txt +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_account.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_billing.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_eelec.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_engine.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_ev2a.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_export_golden.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_integrations.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_interop.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_ledger.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_mce.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_pge_reconcile.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_pge_statements.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_schedules.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_sources_greenbutton.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_sources_ha.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_sources_influx.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_sources_pge.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_trueup.py +0 -0
- {tariffkit-0.2.0 → tariffkit-0.2.2}/tests/test_versioned.py +0 -0
|
@@ -5,6 +5,59 @@ All notable changes to this project are documented here. This project follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.2.2] - 2026-08-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Project documentation now uses a TariffKit banner built from the integration's
|
|
12
|
+
existing icon and a provider-neutral electricity rate curve.
|
|
13
|
+
- Public contribution and support guidance now provides privacy-safe issue
|
|
14
|
+
forms, private security routing, review ownership, a pull request checklist,
|
|
15
|
+
community conduct expectations, and exact development checks. The guidance
|
|
16
|
+
also makes the generated-data and repository-only audit boundaries explicit
|
|
17
|
+
so public collaboration does not expose utility-account material or alter
|
|
18
|
+
distribution guarantees.
|
|
19
|
+
- Pull requests now run GitHub's dependency review action with read-only
|
|
20
|
+
permissions so vulnerable or disallowed dependency changes fail before merge.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **Python 3.14.2 and Home Assistant 2026.3.0 are now the supported floors.**
|
|
24
|
+
The lockfile no longer carries the obsolete Home Assistant 2026.2 fallback,
|
|
25
|
+
and CI audits the complete locked dependency graph with a pinned `pip-audit`
|
|
26
|
+
release while retaining raw reports as failure artifacts. The Linux secrets
|
|
27
|
+
extra and Home Assistant tests both inherit the same exact cryptography pin;
|
|
28
|
+
an expiring policy requires each audit to report exactly its three known,
|
|
29
|
+
unreachable advisories and rejects any additional finding.
|
|
30
|
+
- The README now explains that default HACS approval may take months and gives
|
|
31
|
+
complete custom-repository installation steps for use during the review.
|
|
32
|
+
- Workflow actions are pinned to reviewed immutable commits, checkout credentials
|
|
33
|
+
are not persisted, superseded runs are bounded by concurrency controls, and
|
|
34
|
+
repository write access is isolated to the jobs that publish results.
|
|
35
|
+
- MQTT now rejects credentials over plaintext unless the operator explicitly
|
|
36
|
+
allows insecure authentication for an isolated trusted network. Passwords
|
|
37
|
+
also require a username, while anonymous plaintext publishing remains valid.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- Rate-sheet regeneration now scans trailing table cells with a linear parser,
|
|
41
|
+
avoiding pathological regular-expression backtracking on malformed publisher
|
|
42
|
+
text while preserving accepted dollar, decimal, negative, and change-marker
|
|
43
|
+
forms.
|
|
44
|
+
|
|
45
|
+
## [0.2.1] - 2026-08-16
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- **HACS releases now include a deterministic `tariffkit.zip` integration
|
|
49
|
+
artifact.** HACS installs only the tracked component files, rooted directly
|
|
50
|
+
in the integration directory, while the release pipeline validates the ZIP
|
|
51
|
+
against its source, checksums it with the Python distributions, and attaches
|
|
52
|
+
all artifacts before immutable publication. Dedicated HACS and hassfest
|
|
53
|
+
checks also gate integration changes and prepare TariffKit for default-store
|
|
54
|
+
submission.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
- Home Assistant action descriptions now follow hassfest's current service
|
|
58
|
+
schema: icons live in `icons.json`, target metadata is explicit, and the
|
|
59
|
+
config-entry-only YAML schema is declared.
|
|
60
|
+
|
|
8
61
|
## [0.2.0] - 2026-08-16
|
|
9
62
|
|
|
10
63
|
### Added
|
|
@@ -168,5 +221,7 @@ Initial release.
|
|
|
168
221
|
- Holiday calendars are extracted per vintage from the source data rather than
|
|
169
222
|
recomputed, because the vintage files disagree in far-future years.
|
|
170
223
|
|
|
171
|
-
[Unreleased]: https://github.com/eman/tariffkit/compare/v0.2.
|
|
224
|
+
[Unreleased]: https://github.com/eman/tariffkit/compare/v0.2.2...HEAD
|
|
225
|
+
[0.2.2]: https://github.com/eman/tariffkit/releases/tag/v0.2.2
|
|
226
|
+
[0.2.1]: https://github.com/eman/tariffkit/releases/tag/v0.2.1
|
|
172
227
|
[0.2.0]: https://github.com/eman/tariffkit/releases/tag/v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tariffkit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Offline electricity tariff pricing, billing, and integration tools
|
|
5
5
|
Project-URL: Homepage, https://github.com/eman/tariffkit
|
|
6
6
|
Project-URL: Documentation, https://github.com/eman/tariffkit#documentation
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
18
|
Classifier: Topic :: Home Automation
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
|
-
Requires-Python: >=3.14
|
|
20
|
+
Requires-Python: >=3.14.2
|
|
21
21
|
Provides-Extra: all
|
|
22
22
|
Requires-Dist: fastapi>=0.110; extra == 'all'
|
|
23
23
|
Requires-Dist: httpx>=0.27; extra == 'all'
|
|
@@ -43,8 +43,17 @@ Requires-Dist: fastapi>=0.110; extra == 'web'
|
|
|
43
43
|
Requires-Dist: uvicorn[standard]>=0.29; extra == 'web'
|
|
44
44
|
Description-Content-Type: text/markdown
|
|
45
45
|
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="images/banner.svg" alt="TariffKit — electricity pricing, billing, and energy integrations" width="100%">
|
|
48
|
+
</p>
|
|
49
|
+
|
|
46
50
|
# TariffKit
|
|
47
51
|
|
|
52
|
+
[](https://github.com/eman/tariffkit/actions/workflows/ci.yml)
|
|
53
|
+
[](https://pypi.org/project/tariffkit/)
|
|
54
|
+
[](https://www.python.org/downloads/)
|
|
55
|
+
[](LICENSE)
|
|
56
|
+
|
|
48
57
|
An offline electricity tariff engine for pricing, billing, and energy-system
|
|
49
58
|
integrations. The first data provider supports PG&E residential rate plans under
|
|
50
59
|
**NEM 3.0 / the Net Billing Tariff (NBT)**; the package identity is deliberately
|
|
@@ -134,6 +143,23 @@ pip install 'tariffkit[statements]' # + reading local PG&E statement PDFs
|
|
|
134
143
|
pip install 'tariffkit[all]'
|
|
135
144
|
```
|
|
136
145
|
|
|
146
|
+
The Home Assistant integration has been
|
|
147
|
+
[submitted to the default HACS store](https://github.com/hacs/default/pull/10019),
|
|
148
|
+
but approval queues can take months. Until it is approved, install TariffKit as
|
|
149
|
+
a custom repository:
|
|
150
|
+
|
|
151
|
+
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=eman&repository=tariffkit&category=integration)
|
|
152
|
+
|
|
153
|
+
Or add it manually:
|
|
154
|
+
|
|
155
|
+
1. Open **HACS** in Home Assistant.
|
|
156
|
+
2. Select the **three-dot menu → Custom repositories**.
|
|
157
|
+
3. Enter `https://github.com/eman/tariffkit`, choose **Integration**, and select
|
|
158
|
+
**Add**.
|
|
159
|
+
4. Search HACS for **TariffKit**, open it, and select **Download**.
|
|
160
|
+
5. Restart Home Assistant, then add TariffKit from
|
|
161
|
+
**Settings → Devices & services → Add integration**.
|
|
162
|
+
|
|
137
163
|
## CLI
|
|
138
164
|
|
|
139
165
|
```bash
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="images/banner.svg" alt="TariffKit — electricity pricing, billing, and energy integrations" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# TariffKit
|
|
2
6
|
|
|
7
|
+
[](https://github.com/eman/tariffkit/actions/workflows/ci.yml)
|
|
8
|
+
[](https://pypi.org/project/tariffkit/)
|
|
9
|
+
[](https://www.python.org/downloads/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
3
12
|
An offline electricity tariff engine for pricing, billing, and energy-system
|
|
4
13
|
integrations. The first data provider supports PG&E residential rate plans under
|
|
5
14
|
**NEM 3.0 / the Net Billing Tariff (NBT)**; the package identity is deliberately
|
|
@@ -89,6 +98,23 @@ pip install 'tariffkit[statements]' # + reading local PG&E statement PDFs
|
|
|
89
98
|
pip install 'tariffkit[all]'
|
|
90
99
|
```
|
|
91
100
|
|
|
101
|
+
The Home Assistant integration has been
|
|
102
|
+
[submitted to the default HACS store](https://github.com/hacs/default/pull/10019),
|
|
103
|
+
but approval queues can take months. Until it is approved, install TariffKit as
|
|
104
|
+
a custom repository:
|
|
105
|
+
|
|
106
|
+
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=eman&repository=tariffkit&category=integration)
|
|
107
|
+
|
|
108
|
+
Or add it manually:
|
|
109
|
+
|
|
110
|
+
1. Open **HACS** in Home Assistant.
|
|
111
|
+
2. Select the **three-dot menu → Custom repositories**.
|
|
112
|
+
3. Enter `https://github.com/eman/tariffkit`, choose **Integration**, and select
|
|
113
|
+
**Add**.
|
|
114
|
+
4. Search HACS for **TariffKit**, open it, and select **Download**.
|
|
115
|
+
5. Restart Home Assistant, then add TariffKit from
|
|
116
|
+
**Settings → Devices & services → Add integration**.
|
|
117
|
+
|
|
92
118
|
## CLI
|
|
93
119
|
|
|
94
120
|
```bash
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tariffkit"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Offline electricity tariff pricing, billing, and integration tools"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
|
-
requires-python = ">=3.14"
|
|
12
|
+
requires-python = ">=3.14.2"
|
|
13
13
|
keywords = ["pge", "nem3", "net-billing-tariff", "solar", "battery", "home-assistant", "energy", "tariff"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 3 - Alpha",
|
|
@@ -61,11 +61,15 @@ release = [
|
|
|
61
61
|
"packaging>=25",
|
|
62
62
|
"twine>=6.2",
|
|
63
63
|
]
|
|
64
|
+
security = [
|
|
65
|
+
"pip-audit==2.10.1",
|
|
66
|
+
]
|
|
64
67
|
dev = [
|
|
65
68
|
{ include-group = "test" },
|
|
66
69
|
{ include-group = "quality" },
|
|
67
70
|
{ include-group = "regen" },
|
|
68
71
|
{ include-group = "release" },
|
|
72
|
+
{ include-group = "security" },
|
|
69
73
|
"fastapi>=0.110",
|
|
70
74
|
"paho-mqtt>=2.0",
|
|
71
75
|
"pytest-homeassistant-custom-component>=0.13.316",
|
|
@@ -215,6 +215,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
215
215
|
mqtt.add_argument("--topic-prefix")
|
|
216
216
|
mqtt.add_argument("--forecast-hours", type=int)
|
|
217
217
|
mqtt.add_argument("--tls", action="store_true", default=None)
|
|
218
|
+
mqtt.add_argument(
|
|
219
|
+
"--allow-insecure-auth",
|
|
220
|
+
action="store_true",
|
|
221
|
+
default=None,
|
|
222
|
+
help="allow MQTT credentials without TLS on an isolated trusted network",
|
|
223
|
+
)
|
|
218
224
|
mqtt.add_argument(
|
|
219
225
|
"--no-discovery",
|
|
220
226
|
dest="discovery",
|
|
@@ -601,6 +607,7 @@ def _mqtt_settings(args: Any, *, config: Config | None, profile_name: str | None
|
|
|
601
607
|
discovery=args.discovery,
|
|
602
608
|
forecast_hours=args.forecast_hours,
|
|
603
609
|
tls=args.tls,
|
|
610
|
+
allow_insecure_auth=args.allow_insecure_auth,
|
|
604
611
|
profile=profile_name,
|
|
605
612
|
)
|
|
606
613
|
if config is not None and settings.profile is not None:
|
|
@@ -49,10 +49,26 @@ class MqttSettings:
|
|
|
49
49
|
forecast_hours: int = 48
|
|
50
50
|
client_id: str = "tariffkit"
|
|
51
51
|
tls: bool = False
|
|
52
|
+
allow_insecure_auth: bool = False
|
|
52
53
|
profile: str | None = None
|
|
53
54
|
account: str | None = None
|
|
54
55
|
|
|
55
56
|
def __post_init__(self) -> None:
|
|
57
|
+
if not isinstance(self.tls, bool):
|
|
58
|
+
raise ConfigError("MQTT tls must be a boolean")
|
|
59
|
+
if not isinstance(self.allow_insecure_auth, bool):
|
|
60
|
+
raise ConfigError("MQTT allow_insecure_auth must be a boolean")
|
|
61
|
+
if self.password is not None and not self.username:
|
|
62
|
+
raise ConfigError("MQTT password requires a username")
|
|
63
|
+
if (
|
|
64
|
+
(self.username is not None or self.password is not None)
|
|
65
|
+
and not self.tls
|
|
66
|
+
and not self.allow_insecure_auth
|
|
67
|
+
):
|
|
68
|
+
raise ConfigError(
|
|
69
|
+
"MQTT credentials require TLS; enable tls or explicitly set "
|
|
70
|
+
"allow_insecure_auth for an isolated trusted network"
|
|
71
|
+
)
|
|
56
72
|
if self.profile is not None and self.account is not None and self.profile != self.account:
|
|
57
73
|
raise ConfigError("profile and account selections disagree")
|
|
58
74
|
if self.profile is None:
|
|
@@ -79,6 +95,7 @@ class MqttSettings:
|
|
|
79
95
|
"forecast_hours",
|
|
80
96
|
"client_id",
|
|
81
97
|
"tls",
|
|
98
|
+
"allow_insecure_auth",
|
|
82
99
|
"profile",
|
|
83
100
|
"account",
|
|
84
101
|
):
|
|
@@ -101,6 +118,17 @@ class MqttSettings:
|
|
|
101
118
|
):
|
|
102
119
|
if value := env.get(name):
|
|
103
120
|
values[key] = value
|
|
121
|
+
if value := env.get("TARIFFKIT_MQTT_ALLOW_INSECURE_AUTH"):
|
|
122
|
+
normalized = value.casefold()
|
|
123
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
124
|
+
values["allow_insecure_auth"] = True
|
|
125
|
+
elif normalized in {"0", "false", "no", "off"}:
|
|
126
|
+
values["allow_insecure_auth"] = False
|
|
127
|
+
else:
|
|
128
|
+
raise ConfigError(
|
|
129
|
+
"TARIFFKIT_MQTT_ALLOW_INSECURE_AUTH must be a boolean "
|
|
130
|
+
"(true/false, yes/no, on/off, or 1/0)"
|
|
131
|
+
)
|
|
104
132
|
for name in ("TARIFFKIT_ACCOUNT", "TARIFFKIT_PROFILE"):
|
|
105
133
|
if value := env.get(name):
|
|
106
134
|
values["profile"] = value
|
|
@@ -177,7 +205,7 @@ class MqttPublisher:
|
|
|
177
205
|
Kept separate from construction to allow dependency injection of the client
|
|
178
206
|
while still exercising this logic.
|
|
179
207
|
"""
|
|
180
|
-
if self.settings.username:
|
|
208
|
+
if self.settings.username is not None:
|
|
181
209
|
client.username_pw_set(self.settings.username, self.settings.password)
|
|
182
210
|
if self.settings.tls:
|
|
183
211
|
client.tls_set()
|
|
@@ -21,7 +21,7 @@ from tariffkit.account import (
|
|
|
21
21
|
)
|
|
22
22
|
from tariffkit.account.cli import migrate_existing, sync_profile
|
|
23
23
|
from tariffkit.billing import BillingPeriod, IntervalReading
|
|
24
|
-
from tariffkit.cli import _mqtt_settings, _pricing_context, main
|
|
24
|
+
from tariffkit.cli import _mqtt_settings, _pricing_context, build_parser, main
|
|
25
25
|
from tariffkit.config import Config
|
|
26
26
|
from tariffkit.errors import ConfigError
|
|
27
27
|
from tariffkit.models import Supplier
|
|
@@ -412,6 +412,7 @@ def test_explicit_config_stops_mqtt_from_reverting_to_a_default_profile(
|
|
|
412
412
|
discovery=None,
|
|
413
413
|
forecast_hours=None,
|
|
414
414
|
tls=None,
|
|
415
|
+
allow_insecure_auth=None,
|
|
415
416
|
)
|
|
416
417
|
_engine, config, profile_name, _repository = _pricing_context(args)
|
|
417
418
|
|
|
@@ -420,3 +421,13 @@ def test_explicit_config_stops_mqtt_from_reverting_to_a_default_profile(
|
|
|
420
421
|
settings = _mqtt_settings(args, config=config, profile_name=profile_name)
|
|
421
422
|
|
|
422
423
|
assert settings.profile is None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_mqtt_cli_accepts_insecure_auth_escape_hatch() -> None:
|
|
427
|
+
args = build_parser().parse_args(
|
|
428
|
+
["mqtt", "--broker", "broker.local", "--username", "user", "--allow-insecure-auth"]
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
settings = _mqtt_settings(args, config=None, profile_name=None)
|
|
432
|
+
|
|
433
|
+
assert settings.allow_insecure_auth is True
|
|
@@ -23,9 +23,17 @@ class FakeClient:
|
|
|
23
23
|
self.published: list[tuple[str, str, bool]] = []
|
|
24
24
|
self.will: tuple[str, str, bool] | None = None
|
|
25
25
|
self.connected: tuple[str, int] | None = None
|
|
26
|
+
self.auth: tuple[str, str | None] | None = None
|
|
27
|
+
self.tls = False
|
|
26
28
|
self.loop_running = False
|
|
27
29
|
self.disconnected = False
|
|
28
30
|
|
|
31
|
+
def username_pw_set(self, username: str, password: str | None = None) -> None:
|
|
32
|
+
self.auth = (username, password)
|
|
33
|
+
|
|
34
|
+
def tls_set(self) -> None:
|
|
35
|
+
self.tls = True
|
|
36
|
+
|
|
29
37
|
def will_set(self, topic: str, payload: str, retain: bool = False) -> None:
|
|
30
38
|
self.will = (topic, payload, retain)
|
|
31
39
|
|
|
@@ -82,6 +90,48 @@ def test_discovery_can_be_disabled() -> None:
|
|
|
82
90
|
assert not any(t.startswith("homeassistant/") for t in client_of(publisher).topics())
|
|
83
91
|
|
|
84
92
|
|
|
93
|
+
def test_anonymous_plaintext_connection_is_allowed() -> None:
|
|
94
|
+
publisher = make_publisher()
|
|
95
|
+
|
|
96
|
+
assert client_of(publisher).auth is None
|
|
97
|
+
assert client_of(publisher).tls is False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_authenticated_plaintext_connection_is_rejected() -> None:
|
|
101
|
+
with pytest.raises(ConfigError, match="credentials require TLS"):
|
|
102
|
+
make_publisher(username="user")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_password_without_username_is_rejected_even_with_tls() -> None:
|
|
106
|
+
with pytest.raises(ConfigError, match="password requires a username"):
|
|
107
|
+
make_publisher(password="secret", tls=True)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_insecure_auth_escape_hatch_must_be_boolean() -> None:
|
|
111
|
+
with pytest.raises(ConfigError, match="allow_insecure_auth must be a boolean"):
|
|
112
|
+
make_publisher(username="user", allow_insecure_auth="false")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_authenticated_tls_connection_configures_auth_and_tls() -> None:
|
|
116
|
+
publisher = make_publisher(username="user", password="secret", tls=True, port=8883)
|
|
117
|
+
|
|
118
|
+
client = client_of(publisher)
|
|
119
|
+
assert client.auth == ("user", "secret")
|
|
120
|
+
assert client.tls is True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_explicit_insecure_auth_allows_credentials_without_tls() -> None:
|
|
124
|
+
publisher = make_publisher(
|
|
125
|
+
username="user",
|
|
126
|
+
password="secret",
|
|
127
|
+
allow_insecure_auth=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
client = client_of(publisher)
|
|
131
|
+
assert client.auth == ("user", "secret")
|
|
132
|
+
assert client.tls is False
|
|
133
|
+
|
|
134
|
+
|
|
85
135
|
def test_publishes_known_values_retained(publisher: MqttPublisher) -> None:
|
|
86
136
|
publisher.publish_now(datetime(2026, 9, 15, 19, tzinfo=PACIFIC))
|
|
87
137
|
topics = client_of(publisher).topics()
|
|
@@ -204,6 +254,48 @@ def test_mqtt_settings_read_default_profile_from_shared_config(tmp_path: Path) -
|
|
|
204
254
|
assert settings.profile == "home"
|
|
205
255
|
|
|
206
256
|
|
|
257
|
+
def test_mqtt_settings_loads_insecure_auth_escape_hatch_from_toml(tmp_path: Path) -> None:
|
|
258
|
+
config = tmp_path / "config.toml"
|
|
259
|
+
config.write_text(
|
|
260
|
+
'[mqtt]\nbroker = "broker.local"\nallow_insecure_auth = true\n',
|
|
261
|
+
encoding="utf-8",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
settings = MqttSettings.load(
|
|
265
|
+
config,
|
|
266
|
+
tmp_path / "absent",
|
|
267
|
+
username="user",
|
|
268
|
+
password="secret",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
assert settings.allow_insecure_auth is True
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_mqtt_settings_loads_insecure_auth_escape_hatch_from_environment(
|
|
275
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
276
|
+
) -> None:
|
|
277
|
+
monkeypatch.setenv("TARIFFKIT_MQTT_BROKER", "broker.local")
|
|
278
|
+
monkeypatch.setenv("TARIFFKIT_MQTT_USERNAME", "user")
|
|
279
|
+
monkeypatch.setenv("TARIFFKIT_MQTT_ALLOW_INSECURE_AUTH", "true")
|
|
280
|
+
|
|
281
|
+
settings = MqttSettings.load(tmp_path / "absent", tmp_path / "absent-dotenv")
|
|
282
|
+
|
|
283
|
+
assert settings.allow_insecure_auth is True
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_mqtt_settings_rejects_invalid_insecure_auth_environment_value(
|
|
287
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
288
|
+
) -> None:
|
|
289
|
+
monkeypatch.setenv("TARIFFKIT_MQTT_ALLOW_INSECURE_AUTH", "sometimes")
|
|
290
|
+
|
|
291
|
+
with pytest.raises(ConfigError, match="must be a boolean"):
|
|
292
|
+
MqttSettings.load(
|
|
293
|
+
tmp_path / "absent",
|
|
294
|
+
tmp_path / "absent-dotenv",
|
|
295
|
+
broker="broker.local",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
207
299
|
def test_mqtt_environment_profile_overrides_config_alias(
|
|
208
300
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
209
301
|
) -> None:
|
|
@@ -101,7 +101,10 @@ def test_mqtt_settings_resolve_config_environment_and_keyring(
|
|
|
101
101
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
102
102
|
) -> None:
|
|
103
103
|
config = tmp_path / "config.toml"
|
|
104
|
-
config.write_text(
|
|
104
|
+
config.write_text(
|
|
105
|
+
'[mqtt]\nbroker = "config-broker"\nport = 1884\ntls = true\n',
|
|
106
|
+
encoding="utf-8",
|
|
107
|
+
)
|
|
105
108
|
monkeypatch.setenv("TARIFFKIT_MQTT_BROKER", "env-broker")
|
|
106
109
|
monkeypatch.setattr(
|
|
107
110
|
"tariffkit.mqtt.publisher.get_secret",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tax/ca_energy_resources/2025-01-01.toml
RENAMED
|
File without changes
|
{tariffkit-0.2.0 → tariffkit-0.2.2}/src/tariffkit/data/tax/ca_energy_resources/2026-01-01.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|