mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
150 lines
4.8 KiB
JavaScript
150 lines
4.8 KiB
JavaScript
// Reward Definition: Darkness
|
|
// Purpose: Defines the darkness reward that alters visibility/lighting behavior. Scope: Encapsulates reward metadata and effect configuration for runtime execution.
|
|
const DURATION_MS = 15 * 60 * 1000;
|
|
|
|
let activeTimer = null;
|
|
let headlightLockUntil = 0;
|
|
|
|
function isHeadlightBlocked() {
|
|
return Date.now() < headlightLockUntil;
|
|
}
|
|
|
|
function clearTimers() {
|
|
if (activeTimer) {
|
|
clearTimeout(activeTimer);
|
|
activeTimer = null;
|
|
}
|
|
}
|
|
|
|
async function forceAllLightsOff(ctx) {
|
|
const entities = ctx.getHomeAssistantEntities();
|
|
await Promise.all(
|
|
entities.map(async (entity) => {
|
|
try {
|
|
await ctx.setHomeAssistantEntityState(entity.id, 'off');
|
|
} catch (err) {
|
|
ctx.logger.warn('darkness force light off failed', { entityId: entity.id, error: err.message });
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function stopDarkness(ctx, effect = {}) {
|
|
clearTimers();
|
|
headlightLockUntil = 0;
|
|
|
|
const prevLights = Array.isArray(effect.prevLights) ? effect.prevLights : [];
|
|
await Promise.all(
|
|
prevLights.map(async (entry) => {
|
|
try {
|
|
await ctx.setHomeAssistantEntityState(entry.id, entry.state === 'on' ? 'on' : 'off');
|
|
} catch (err) {
|
|
ctx.logger.warn('darkness restore light failed', { entityId: entry.id, error: err.message });
|
|
}
|
|
}),
|
|
);
|
|
|
|
try {
|
|
const prevLockState = effect?.prevLightLockState;
|
|
if (prevLockState === 'on' || prevLockState === 'off') {
|
|
await ctx.setHomeAssistantLightsLockedOn(true, {
|
|
source: 'buttonbox:darknessRestore',
|
|
targetState: prevLockState,
|
|
});
|
|
} else {
|
|
await ctx.setHomeAssistantLightsLockedOn(false, {
|
|
source: 'buttonbox:darknessRestore',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
ctx.logger.warn('darkness restore light lock failed', { error: err.message });
|
|
}
|
|
|
|
const prevHeadlights = effect.prevHeadlights && typeof effect.prevHeadlights === 'object'
|
|
? effect.prevHeadlights
|
|
: {};
|
|
Object.entries(prevHeadlights).forEach(([roverId, wasOn]) => {
|
|
try {
|
|
ctx.issueCommand(String(roverId), {
|
|
type: 'headlight',
|
|
headlight: { action: Boolean(wasOn) ? 'on' : 'off' },
|
|
});
|
|
} catch (err) {
|
|
ctx.logger.warn('darkness restore headlight failed', { roverId, error: err.message });
|
|
}
|
|
});
|
|
|
|
ctx.clearEffect('darkness');
|
|
}
|
|
|
|
async function startDarkness(ctx, effect) {
|
|
clearTimers();
|
|
const endsAt = Number(effect.endsAt || Date.now() + DURATION_MS);
|
|
const remaining = Math.max(0, endsAt - Date.now());
|
|
headlightLockUntil = endsAt;
|
|
try {
|
|
await ctx.setHomeAssistantLightsLockedOn(true, {
|
|
source: 'buttonbox:darkness',
|
|
targetState: 'off',
|
|
});
|
|
} catch (err) {
|
|
ctx.logger.warn('darkness set light lock failed', { error: err.message });
|
|
}
|
|
ctx.saveEffect('darkness', effect);
|
|
|
|
/*
|
|
Darkness locks the room-light policy off and performs the initial off
|
|
command through setHomeAssistantLightsLockedOn above. It deliberately does
|
|
not keep a polling interval that re-forces Home Assistant entities off:
|
|
after the lock is established, out-of-band manual controls must remain able
|
|
to change individual room lights without the server fighting them.
|
|
*/
|
|
|
|
activeTimer = setTimeout(() => {
|
|
stopDarkness(ctx, effect).catch((err) => {
|
|
ctx.logger.warn('darkness stop failed', { error: err.message });
|
|
});
|
|
}, remaining);
|
|
}
|
|
|
|
module.exports = {
|
|
id: 'darkness',
|
|
name: 'Darkness',
|
|
description: 'Disables room lights and headlights for 15 minutes.',
|
|
isHeadlightBlocked,
|
|
goal: 400,
|
|
async run(ctx) {
|
|
const entities = ctx.getHomeAssistantEntities();
|
|
const prevLights = entities.map((entity) => ({ id: entity.id, state: entity.state === 'on' ? 'on' : 'off' }));
|
|
await forceAllLightsOff(ctx);
|
|
|
|
const prevHeadlights = {};
|
|
ctx.listOnlineRovers().forEach((rover) => {
|
|
const state = rover?.headlight?.state;
|
|
const headlightOn = Boolean(state && state.headlightOn === true);
|
|
prevHeadlights[String(rover.id)] = headlightOn;
|
|
try {
|
|
ctx.issueCommand(String(rover.id), {
|
|
type: 'headlight',
|
|
headlight: { action: 'off' },
|
|
});
|
|
} catch (err) {
|
|
ctx.logger.warn('darkness headlight off failed', { roverId: rover.id, error: err.message });
|
|
}
|
|
});
|
|
|
|
const prevPolicy = ctx.getHomeAssistantLightPolicy?.() || null;
|
|
const prevLightLockState = prevPolicy?.lockState || (prevPolicy?.lockedOn ? 'on' : null);
|
|
const effect = { endsAt: Date.now() + DURATION_MS, prevLights, prevHeadlights, prevLightLockState };
|
|
await startDarkness(ctx, effect);
|
|
ctx.sendAlert({ color: '#212121', title: 'Darkness', message: 'Darkness effect active for 120 seconds.' });
|
|
},
|
|
async recover(ctx, effect) {
|
|
if (!effect || Number(effect.endsAt || 0) <= Date.now()) {
|
|
await stopDarkness(ctx, effect || {});
|
|
return;
|
|
}
|
|
await startDarkness(ctx, effect);
|
|
},
|
|
};
|