idle service and lght lock improvements

This commit is contained in:
legop3
2026-07-14 17:38:17 -04:00
parent 0d6b4d68de
commit 7fe5730953
14 changed files with 126 additions and 49 deletions
@@ -35,11 +35,26 @@ function registerHomeAssistantHooks(deps) {
return true;
}
function isBlockedByRoomControlLock() {
/*
The lock is meant to keep normal users and automated room-control
surfaces from changing the preferred room-light policy. Admins are the
exception because they may need to correct a single lamp, verify a Home
Assistant integration, or make an operational adjustment while the
public controls remain locked.
This server-side bypass is the authoritative rule. The React UI also
enables admin controls for usability, but clients are not trusted to
enforce permissions.
*/
return isLightControlLocked() && !isAdmin(socket);
}
socket.on('homeAssistant:toggle', async ({ entityId } = {}, cb = () => {}) => {
if (!hasPermission()) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isLightControlLocked()) {
if (isBlockedByRoomControlLock()) {
return cb({ error: 'Room controls are locked' });
}
try {
@@ -55,7 +70,7 @@ function registerHomeAssistantHooks(deps) {
if (!hasPermission()) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isLightControlLocked()) {
if (isBlockedByRoomControlLock()) {
return cb({ error: 'Room controls are locked' });
}
try {
@@ -71,7 +86,7 @@ function registerHomeAssistantHooks(deps) {
if (!hasPermission()) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isLightControlLocked()) {
if (isBlockedByRoomControlLock()) {
return cb({ error: 'Room controls are locked' });
}
try {
@@ -90,7 +105,7 @@ function registerHomeAssistantHooks(deps) {
if (!hasPermission()) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isLightControlLocked()) {
if (isBlockedByRoomControlLock()) {
return cb({ error: 'Room controls are locked' });
}
try {
@@ -426,14 +426,26 @@ function createRuntimeEngine(deps) {
async function setLightsLockedOn(nextValue, options = {}) {
const next = Boolean(nextValue);
const targetState = options?.targetState === 'off' ? 'off' : 'on';
const forceApply = Boolean(options.forceApply);
const nextLockState = next ? targetState : null;
const changed = runtime.lightsLockState !== nextLockState;
runtime.lightsLockState = nextLockState;
if (runtime.lightsLockState != null) {
if ((changed || forceApply) && enabled) {
if (changed && enabled) {
const source = String(options?.source || 'homeAssistant:setLightsLockedOn');
/*
A room-light lock is a policy boundary, not an ongoing reconciliation
loop. Entering locked-on or locked-off sets every configured room
control to the preferred state once so the room starts from the
requested condition. After that first transition, the server leaves
Home Assistant alone so out-of-band controls such as wall switches,
Home Assistant dashboards, or vendor apps can still adjust individual
lights without being periodically overwritten.
Older callers may still pass forceApply from the previous behavior.
It is intentionally ignored here because repeated lock requests must
not become repeated light commands.
*/
if (runtime.lightsLockState === 'on') {
// The lock-on path is intentionally stronger than a normal bulk
// turn_on. It makes actual light entities white while still turning