Home Assistant: add environment variable configuration support

- Add HA_URL and HA_TOKEN environment variables for automatic HA
  integration configuration in HA add-on deployments
- Environment variables always override database settings with
  non-negotiable precedence; database values preserved for fallback
- Auto-enable integration when both env vars are set; partial config
  (one env var) uses database enable state without auto-enabling
- Add centralized get_homeassistant_settings() function following
  Spoolman pattern; replace direct database queries across codebase
- Add ha_url_from_env, ha_token_from_env, ha_env_managed fields to
  AppSettings schema to inform frontend about configuration source
- UI shows read-only fields with lock icons and "(Environment Managed)"
  labels when env-controlled; toggle shows auto-enable badge
- Add comprehensive test coverage: 9 integration + 8 unit tests

Closes #283
This commit is contained in:
bambuman
2026-02-07 19:01:04 +02:00
parent 833a0c62d2
commit eda1f5a9e7
10 changed files with 622 additions and 42 deletions
+3
View File
@@ -751,6 +751,9 @@ export interface AppSettings {
ha_enabled: boolean;
ha_url: string;
ha_token: string;
ha_url_from_env: boolean;
ha_token_from_env: boolean;
ha_env_managed: boolean;
// File Manager / Library settings
library_archive_mode: 'always' | 'never' | 'ask';
library_disk_warning_gb: number;
+67 -19
View File
@@ -1974,18 +1974,29 @@ export function SettingsPage() {
</p>
<div className="flex items-center justify-between">
<div>
<div className="flex-1">
<p className="text-white">{t('settings.enableHomeAssistant')}</p>
<p className="text-xs text-bambu-gray">{t('settings.homeAssistantDescription')}</p>
{localSettings.ha_env_managed && (
<div className="flex items-center gap-1 mt-1">
<Lock className="w-3 h-3 text-bambu-green" />
<span className="text-xs text-bambu-green">
Automatically enabled via environment variables
</span>
</div>
)}
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={localSettings.ha_enabled ?? false}
onChange={(e) => updateSetting('ha_enabled', e.target.checked)}
disabled={localSettings.ha_env_managed}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
<div className={`w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green ${
localSettings.ha_env_managed ? 'opacity-60 cursor-not-allowed' : ''
}`}></div>
</label>
</div>
@@ -1994,30 +2005,67 @@ export function SettingsPage() {
<div>
<label className="block text-sm text-bambu-gray mb-1">
Home Assistant URL
{localSettings.ha_url_from_env && (
<span className="ml-2 text-xs text-bambu-green">
(Environment Managed)
</span>
)}
</label>
<input
type="text"
value={localSettings.ha_url ?? ''}
onChange={(e) => updateSetting('ha_url', e.target.value)}
placeholder="http://192.168.1.100:8123"
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
<div className="relative">
<input
type="text"
value={localSettings.ha_url ?? ''}
onChange={(e) => updateSetting('ha_url', e.target.value)}
placeholder="http://192.168.1.100:8123"
disabled={localSettings.ha_url_from_env}
className={`w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none ${
localSettings.ha_url_from_env ? 'opacity-60 cursor-not-allowed' : ''
}`}
/>
{localSettings.ha_url_from_env && (
<Lock className="absolute right-3 top-2.5 w-4 h-4 text-bambu-gray" />
)}
</div>
{localSettings.ha_url_from_env && (
<p className="text-xs text-bambu-gray mt-1">
Value set by HA_URL environment variable (read-only)
</p>
)}
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">
Long-Lived Access Token
{localSettings.ha_token_from_env && (
<span className="ml-2 text-xs text-bambu-green">
(Environment Managed)
</span>
)}
</label>
<input
type="password"
value={localSettings.ha_token ?? ''}
onChange={(e) => updateSetting('ha_token', e.target.value)}
placeholder="eyJ0eXAiOiJKV1QiLC..."
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
<p className="text-xs text-bambu-gray mt-1">
Create a token in HA: Profile → Long-Lived Access Tokens → Create Token
</p>
<div className="relative">
<input
type="password"
value={localSettings.ha_token ?? ''}
onChange={(e) => updateSetting('ha_token', e.target.value)}
placeholder="eyJ0eXAiOiJKV1QiLC..."
disabled={localSettings.ha_token_from_env}
className={`w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none ${
localSettings.ha_token_from_env ? 'opacity-60 cursor-not-allowed' : ''
}`}
/>
{localSettings.ha_token_from_env && (
<Lock className="absolute right-3 top-2.5 w-4 h-4 text-bambu-gray" />
)}
</div>
{localSettings.ha_token_from_env ? (
<p className="text-xs text-bambu-gray mt-1">
Value set by HA_TOKEN environment variable (read-only)
</p>
) : (
<p className="text-xs text-bambu-gray mt-1">
Create a token in HA: Profile → Long-Lived Access Tokens → Create Token
</p>
)}
</div>
{localSettings.ha_url && localSettings.ha_token && (