-
-
Notifications
You must be signed in to change notification settings - Fork 99
Add Home Assistant secrets.yaml support for API keys and passwords #3128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
63b3d4c
963f8a0
5e39ff3
6ec4ef5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # ----------------------------------------------------------------------------- | ||
| # Predbat secrets.yaml template | ||
| # Copyright Trefor Southwell 2025 - All Rights Reserved | ||
| # This application maybe used for personal use only and not for commercial use | ||
| # ----------------------------------------------------------------------------- | ||
| # | ||
| # This file stores sensitive information like API keys, passwords, and tokens | ||
| # that should not be stored directly in apps.yaml. | ||
| # | ||
| # Store this file in one of these locations (checked in priority order): | ||
| # 1. Path specified in PREDBAT_SECRETS_FILE environment variable | ||
| # 2. secrets.yaml in the same directory as your apps.yaml | ||
| # 3. /config/secrets.yaml (standard Home Assistant location) | ||
| # | ||
| # To use secrets in apps.yaml, reference them with the !secret tag: | ||
| # octopus_api_key: !secret octopus_api_key | ||
| # | ||
| # This will replace !secret octopus_api_key with the actual value below. | ||
| # | ||
| # DO NOT commit this file to version control or share it publicly! | ||
| # | ||
| # ----------------------------------------------------------------------------- | ||
|
|
||
| # Enable debug logging to show where secrets are loaded from | ||
| # (does NOT print actual secret values) | ||
| # logger: debug | ||
|
|
||
| # Octopus Energy API key | ||
| # Get your API key from: https://octopus.energy/dashboard/new/accounts/personal-details/api-access | ||
| #octopus_api_key: "sk_live_YOUR_API_KEY_HERE" | ||
|
|
||
| # Solcast API key | ||
| # Get your API key from: https://toolkit.solcast.com.au/ | ||
| #solcast_api_key: "YOUR_SOLCAST_API_KEY_HERE" | ||
|
|
||
| # Get your Forecast.Solar API key from: https://forecast.solar/ | ||
| #forecast_solar_api_key: "YOUR_FORECAST_SOLAR_API_KEY_HERE" | ||
|
|
||
| # GivEnergy API key (if using GE Cloud) | ||
| # Get your API key from: https://givenergy.cloud/ | ||
| #ge_cloud_key: "YOUR_GIVENERGY_API_KEY_HERE" | ||
|
|
||
| # Fox ESS API key and username (if using Fox Cloud) | ||
| # Get your API key from: https://fox-ess.com/en/fox-cloud/ | ||
| #fox_key: "YOUR_FOX_API_KEY_HERE" | ||
|
|
||
| # Axle API key (if using Axle VPP) | ||
| # Get your API key from: https://axle.energy/ | ||
| #axle_api_key: "YOUR_AXLE_API_KEY_HERE" | ||
|
|
||
| # Home Assistant Long-Lived Access Token | ||
| # Create a long-lived access token in Home Assistant under your user profile | ||
| #ha_key: "YOUR_HOME_ASSISTANT_LONG_LIVED_ACCESS_TOKEN" |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,113 @@ | ||||||||||||||
| # ----------------------------------------------------------------------------- | ||||||||||||||
| # Predbat Home Battery System | ||||||||||||||
| # Copyright Trefor Southwell 2025 - All Rights Reserved | ||||||||||||||
| # This application maybe used for personal use only and not for commercial use | ||||||||||||||
| # ----------------------------------------------------------------------------- | ||||||||||||||
| # fmt off | ||||||||||||||
| # pylint: disable=consider-using-f-string | ||||||||||||||
| # pylint: disable=line-too-long | ||||||||||||||
| # pylint: disable=attribute-defined-outside-init | ||||||||||||||
|
|
||||||||||||||
| import os | ||||||||||||||
| import yaml | ||||||||||||||
|
||||||||||||||
| import tempfile | ||||||||||||||
| from hass import Hass | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def test_secrets_loading(): | ||||||||||||||
| """ | ||||||||||||||
| Test secrets loading mechanism | ||||||||||||||
| """ | ||||||||||||||
| print("**** Running test_secrets_loading ****") | ||||||||||||||
|
|
||||||||||||||
| # Test 1: No secrets file - should work without error | ||||||||||||||
| print(" Test 1: No secrets file") | ||||||||||||||
| if os.path.exists("secrets.yaml"): | ||||||||||||||
| os.remove("secrets.yaml") | ||||||||||||||
| if os.path.exists("/config/secrets.yaml"): | ||||||||||||||
| os.remove("/config/secrets.yaml") | ||||||||||||||
|
||||||||||||||
| os.remove("/config/secrets.yaml") | |
| try: | |
| os.remove("/config/secrets.yaml") | |
| except OSError: | |
| # Ignore errors if /config is not writable or file cannot be removed | |
| pass |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test 2 doesn't clean up the secrets.yaml file if the assertion fails. If the assertion on line 45 fails, line 46 (os.remove) won't execute, leaving secrets.yaml in place for subsequent tests.
Consider using a try/finally block:
with open("secrets.yaml", "w") as f:
yaml.dump(secrets_data, f)
try:
h = Hass()
assert h.secrets == secrets_data, f"Expected {secrets_data}, got {h.secrets}"
print(" PASS - Secrets loaded from current directory")
finally:
if os.path.exists("secrets.yaml"):
os.remove("secrets.yaml")
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test 3 doesn't clean up the environment variable and temp file if the assertion fails. If the assertion on line 57 fails, lines 58-59 won't execute, potentially affecting subsequent tests or leaving temporary files on the system.
Consider using a try/finally block:
os.environ["PREDBAT_SECRETS_FILE"] = temp_secrets_file
try:
h = Hass()
assert h.secrets == secrets_data, f"Expected {secrets_data}, got {h.secrets}"
print(" PASS - Secrets loaded from PREDBAT_SECRETS_FILE")
finally:
if "PREDBAT_SECRETS_FILE" in os.environ:
del os.environ["PREDBAT_SECRETS_FILE"]
if os.path.exists(temp_secrets_file):
os.remove(temp_secrets_file)
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable test_config is not used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The yaml.add_constructor is being called globally for yaml.SafeLoader in the init method. This modifies a global YAML loader state and could cause issues if multiple Hass instances are created (as happens in the test suite). Each new instance will re-register the constructor, potentially causing side effects.
While this may work in practice since the constructor does the same thing, it's better practice to either:
This is particularly important given that test_secrets.py creates 5 separate Hass() instances, each re-registering the global constructor.