home assistant always turn lights on at full brightness trying to fix weird bulb

This commit is contained in:
legop3
2026-08-11 21:25:16 -04:00
parent e5830533ba
commit b5e8d775a2
2 changed files with 114 additions and 3 deletions
@@ -80,6 +80,27 @@ function normalizeRgbColor(color) {
function createRuntimeEngine(deps) {
const { logger, enabled, haConfig, callHomeAssistantService } = deps;
async function turnOnLightAtFullBrightness(entityId, serviceData = {}) {
/*
Every server-owned interaction that turns on or changes a light must
also restore it to full brightness. Home Assistant remembers a bulb's
previous brightness, so sending only a color or color temperature can
otherwise make a light appear unexpectedly dim even though this service
requested an on-state.
Keeping this rule in one helper makes it apply consistently to ordinary
on commands, RGB changes, white-temperature changes, bulk operations,
random scenes, and lock-on behavior. brightness_pct is deliberately
written after the caller's service data so future call sites cannot
accidentally override the service-wide 100 percent requirement.
*/
await callHomeAssistantService('light', 'turn_on', {
entity_id: entityId,
...serviceData,
brightness_pct: 100,
});
}
function emitUpdate(getState) {
events.emit('update', getState());
}
@@ -143,7 +164,14 @@ function createRuntimeEngine(deps) {
const domain = String(meta.domain || (meta.type === 'light' ? 'light' : 'switch')).toLowerCase();
const service = nextState === 'on' ? 'turn_on' : 'turn_off';
const source = String(options?.source || 'unknown');
await callHomeAssistantService(domain, service, { entity_id: entityId });
if (domain === 'light' && service === 'turn_on') {
await turnOnLightAtFullBrightness(entityId);
} else {
// Off commands and non-light domains do not accept a meaningful light
// brightness value, so their existing Home Assistant payload stays
// intentionally unchanged.
await callHomeAssistantService(domain, service, { entity_id: entityId });
}
logger.info('Issued Home Assistant command', { entityId, domain, service, source });
}
@@ -467,7 +495,7 @@ function createRuntimeEngine(deps) {
if (!runtime.connection) throw new Error('Home Assistant not connected');
const normalized = normalizeRgbColor(color);
await callHomeAssistantService('light', 'turn_on', { entity_id: entityId, rgb_color: normalized });
await turnOnLightAtFullBrightness(entityId, { rgb_color: normalized });
logger.info('Issued Home Assistant color command', { entityId, rgbColor: normalized });
}
@@ -481,7 +509,7 @@ function createRuntimeEngine(deps) {
const normalizedKelvin = Number.isFinite(nextKelvin)
? Math.max(2000, Math.min(6500, Math.round(nextKelvin)))
: DEFAULT_WHITE_KELVIN;
await callHomeAssistantService('light', 'turn_on', { entity_id: entityId, color_temp_kelvin: normalizedKelvin });
await turnOnLightAtFullBrightness(entityId, { color_temp_kelvin: normalizedKelvin });
logger.info('Issued Home Assistant white command', { entityId, colorTempKelvin: normalizedKelvin });
}
@@ -0,0 +1,83 @@
// Home Assistant Runtime Engine Tests
// Purpose: Verifies the service-wide full-brightness rule for light commands.
// Scope: Exercises injected Home Assistant calls without opening a real connection or starting the server.
const assert = require('node:assert/strict');
const test = require('node:test');
const { createRuntimeEngine } = require('./runtimeEngine');
const { entityConfig, entityState, runtime } = require('./state');
function createHarness() {
const calls = [];
const engine = createRuntimeEngine({
enabled: true,
haConfig: { whiteKelvin: 4000 },
callHomeAssistantService: async (domain, service, serviceData) => {
calls.push({ domain, service, serviceData });
},
// These tests only verify outbound service payloads. A no-op logger keeps
// the harness faithful to the runtime dependency contract without adding
// unrelated output to the test run.
logger: {
info() {},
warn() {},
},
});
return { calls, engine };
}
test('light interactions force full brightness without changing switches or off commands', async (t) => {
const { calls, engine } = createHarness();
/*
runtimeEngine uses the shared entity registry populated from configuration
in production. Seed the smallest representative registry here and restore
the shared state afterward so this focused unit test cannot leak state into
other Home Assistant tests added later.
*/
entityConfig.clear();
entityState.clear();
entityConfig.set('light.room', { id: 'light.room', type: 'light', domain: 'light' });
entityConfig.set('switch.lamp', { id: 'switch.lamp', type: 'switch', domain: 'switch' });
runtime.connection = {};
t.after(() => {
entityConfig.clear();
entityState.clear();
runtime.connection = null;
});
await engine.setEntityState('light.room', 'on');
await engine.setLightColor('light.room', [12, 34, 56]);
await engine.setLightWhite('light.room', 4500);
await engine.setEntityState('light.room', 'off');
await engine.setEntityState('switch.lamp', 'on');
assert.deepEqual(calls, [
{
domain: 'light',
service: 'turn_on',
serviceData: { entity_id: 'light.room', brightness_pct: 100 },
},
{
domain: 'light',
service: 'turn_on',
serviceData: { entity_id: 'light.room', rgb_color: [12, 34, 56], brightness_pct: 100 },
},
{
domain: 'light',
service: 'turn_on',
serviceData: { entity_id: 'light.room', color_temp_kelvin: 4500, brightness_pct: 100 },
},
{
domain: 'light',
service: 'turn_off',
serviceData: { entity_id: 'light.room' },
},
{
domain: 'switch',
service: 'turn_on',
serviceData: { entity_id: 'switch.lamp' },
},
]);
});