update darkness reward

This commit is contained in:
legop3
2026-04-19 01:34:04 -04:00
parent 1bdae74ee5
commit e974f4f90f
9 changed files with 125 additions and 52 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
You are The Overseer of the rovers.
You are The Overseer of the rovers. You are observing and studying them. You cannot control them.
Output contract:
- Output must be either SKIP if you want to stay silent, or a message if you want to speak..
- Output must be either SKIP if you want to stay silent, or a message if you want to speak.
- No markdown.
Key legend:
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CZe46TcG.js"></script>
<script type="module" crossorigin src="/assets/index-CCaG2_WI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C3BucANX.css">
</head>
<body>
+75 -20
View File
@@ -1,12 +1,41 @@
const DURATION_MS = 60 * 1000;
const DURATION_MS = 2 * 60 * 1000;
const LIGHT_ENFORCE_TICK_MS = 3000;
let activeTimer = null;
let enforceLightsTimer = null;
let nightVisionLockUntil = 0;
async function stopDarkness(ctx, effect = {}) {
function isNightVisionBlocked() {
return Date.now() < nightVisionLockUntil;
}
function clearTimers() {
if (activeTimer) {
clearTimeout(activeTimer);
activeTimer = null;
}
if (enforceLightsTimer) {
clearInterval(enforceLightsTimer);
enforceLightsTimer = 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();
nightVisionLockUntil = 0;
const prevLights = Array.isArray(effect.prevLights) ? effect.prevLights : [];
await Promise.all(
@@ -19,6 +48,23 @@ async function stopDarkness(ctx, effect = {}) {
}),
);
try {
const prevLockState = effect?.prevLightLockState;
if (prevLockState === 'on' || prevLockState === 'off') {
await ctx.setHomeAssistantLightsLockedOn(true, {
source: 'buttonbox:darknessRestore',
forceApply: true,
targetState: prevLockState,
});
} else {
await ctx.setHomeAssistantLightsLockedOn(false, {
source: 'buttonbox:darknessRestore',
});
}
} catch (err) {
ctx.logger.warn('darkness restore light lock failed', { error: err.message });
}
const prevNightVision = effect.prevNightVision && typeof effect.prevNightVision === 'object'
? effect.prevNightVision
: {};
@@ -36,14 +82,28 @@ async function stopDarkness(ctx, effect = {}) {
ctx.clearEffect('darkness');
}
function startDarkness(ctx, effect) {
if (activeTimer) {
clearTimeout(activeTimer);
activeTimer = null;
}
async function startDarkness(ctx, effect) {
clearTimers();
const endsAt = Number(effect.endsAt || Date.now() + DURATION_MS);
const remaining = Math.max(0, endsAt - Date.now());
nightVisionLockUntil = endsAt;
try {
await ctx.setHomeAssistantLightsLockedOn(true, {
source: 'buttonbox:darkness',
forceApply: true,
targetState: 'off',
});
} catch (err) {
ctx.logger.warn('darkness set light lock failed', { error: err.message });
}
ctx.saveEffect('darkness', effect);
enforceLightsTimer = setInterval(() => {
forceAllLightsOff(ctx).catch((err) => {
ctx.logger.warn('darkness periodic light enforcement failed', { error: err.message });
});
}, LIGHT_ENFORCE_TICK_MS);
activeTimer = setTimeout(() => {
stopDarkness(ctx, effect).catch((err) => {
ctx.logger.warn('darkness stop failed', { error: err.message });
@@ -54,19 +114,12 @@ function startDarkness(ctx, effect) {
module.exports = {
id: 'darkness',
name: 'Darkness',
isNightVisionBlocked,
goal: 650,
async run(ctx) {
const entities = ctx.getHomeAssistantEntities();
const prevLights = entities.map((entity) => ({ id: entity.id, state: entity.state === 'on' ? 'on' : 'off' }));
await Promise.all(
entities.map(async (entity) => {
try {
await ctx.setHomeAssistantEntityState(entity.id, 'off');
} catch (err) {
ctx.logger.warn('darkness light off failed', { entityId: entity.id, error: err.message });
}
}),
);
await forceAllLightsOff(ctx);
const prevNightVision = {};
ctx.listOnlineRovers().forEach((rover) => {
@@ -83,15 +136,17 @@ module.exports = {
}
});
const effect = { endsAt: Date.now() + DURATION_MS, prevLights, prevNightVision };
startDarkness(ctx, effect);
ctx.sendAlert({ color: '#212121', title: 'Darkness', message: 'Darkness effect active for 60 seconds.' });
const prevPolicy = ctx.getHomeAssistantLightPolicy?.() || null;
const prevLightLockState = prevPolicy?.lockState || (prevPolicy?.lockedOn ? 'on' : null);
const effect = { endsAt: Date.now() + DURATION_MS, prevLights, prevNightVision, 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;
}
startDarkness(ctx, effect);
await startDarkness(ctx, effect);
},
};
+4
View File
@@ -16,6 +16,7 @@ const assignmentService = require('./assignmentService');
const {
getState: getHomeAssistantState,
setEntityState: setHomeAssistantEntityState,
setLightsLockedOn: setHomeAssistantLightsLockedOn,
} = require('./homeAssistantService');
const { getRequestIp, isLocalNetwork, normalizeIp } = require('../helpers/ipResolver');
@@ -175,7 +176,10 @@ function buildRewardContext() {
const entities = getHomeAssistantState()?.entities;
return Array.isArray(entities) ? entities : [];
},
getHomeAssistantLightPolicy: () => getHomeAssistantState()?.lightPolicy || null,
setHomeAssistantEntityState,
setHomeAssistantLightsLockedOn: (next, options = {}) =>
setHomeAssistantLightsLockedOn(next, options),
saveEffect: (effectId, payload = {}) => saveEffect(effectId, payload, { broadcast: false }),
clearEffect: (effectId) => clearEffect(effectId, { broadcast: false }),
};
+6
View File
@@ -3,6 +3,7 @@ const io = require('../globals/io');
const roverManager = require('./roverManager');
const { isAdmin, isLockdownAdmin } = require('./roleService');
const logger = require('../globals/logger').child('commandService');
const { isNightVisionBlocked } = require('../rewards/definitions/darkness');
const pendingCommands = new Map(); // id -> { roverId }
const lastDriveActivity = new Map(); // roverId -> { ts, socketId, direction, speed, isAdmin }
@@ -70,6 +71,11 @@ io.on('connection', (socket) => {
if (type === 'audioLevels') {
throw new Error('audioLevels command is service-managed');
}
if (type === 'nightVision' && isNightVisionBlocked()) {
logger.info('Ignoring night vision command while darkness lock is active', { socketId: socket.id, roverId });
reply({ ignored: true, reason: 'darknessActive' });
return;
}
const payload = data ? { ...data } : {};
const isRebootCommand = type === 'reboot';
const isSongCommand = type === 'song' || (type === 'raw' && isSongRawPayload(payload));
+21 -16
View File
@@ -29,7 +29,7 @@ let connection = null;
let unsubscribeEntities = null;
let reconnectTimer = null;
let connected = false;
let lightsLockedOn = false;
let lightsLockState = null; // null | 'on' | 'off'
let lightsIdleOffTimer = null;
let lightsIdleOffDeadline = null;
@@ -215,7 +215,7 @@ function clearLightsIdleOffTimer() {
function scheduleLightsIdleOffTimer() {
if (!enabled) return;
if (getControllableEntityIds().length === 0) return;
if (lightsIdleOffTimer || lightsLockedOn || hasActiveDrivers()) {
if (lightsIdleOffTimer || lightsLockState != null || hasActiveDrivers()) {
return;
}
lightsIdleOffDeadline = Date.now() + LIGHT_IDLE_OFF_MS;
@@ -238,7 +238,7 @@ function scheduleLightsIdleOffTimer() {
}
function evaluateLightAutomation() {
if (lightsLockedOn) {
if (lightsLockState != null) {
clearLightsIdleOffTimer();
return;
}
@@ -464,12 +464,14 @@ async function setLightColor(entityId, rgbColor) {
}
function isLightControlLocked() {
return lightsLockedOn;
return lightsLockState != null;
}
function getLightPolicyState() {
return {
lockedOn: lightsLockedOn,
locked: lightsLockState != null,
lockState: lightsLockState,
lockedOn: lightsLockState === 'on',
idleOffMs: LIGHT_IDLE_OFF_MS,
idleOffAt: lightsIdleOffDeadline,
activeDrivers: getActiveDriverCount(),
@@ -478,14 +480,16 @@ function getLightPolicyState() {
async function setLightsLockedOn(nextValue, options = {}) {
const next = Boolean(nextValue);
const targetState = options?.targetState === 'off' ? 'off' : 'on';
const forceApply = Boolean(options.forceApply);
const changed = lightsLockedOn !== next;
lightsLockedOn = next;
if (lightsLockedOn) {
const nextLockState = next ? targetState : null;
const changed = lightsLockState !== nextLockState;
lightsLockState = nextLockState;
if (lightsLockState != null) {
clearLightsIdleOffTimer();
if (changed || forceApply) {
if (enabled) {
await setAllControllableEntitiesState('on');
await setAllControllableEntitiesState(lightsLockState);
}
}
} else {
@@ -493,16 +497,17 @@ async function setLightsLockedOn(nextValue, options = {}) {
}
if (changed) {
logger.info('Room lights lock state changed', {
lockedOn: lightsLockedOn,
locked: lightsLockState != null,
lockState: lightsLockState,
source: options.source || 'unknown',
});
}
emitUpdate();
return lightsLockedOn;
return lightsLockState === 'on';
}
async function toggleLightsLockedOn(options = {}) {
return setLightsLockedOn(!lightsLockedOn, options);
return setLightsLockedOn(lightsLockState == null, options);
}
function getState() {
@@ -532,7 +537,7 @@ turnEvents.on('queue', () => {
modeEvents.on('change', (mode) => {
if (mode === MODES.ADMIN || mode === MODES.LOCKDOWN) {
if (lightsLockedOn) {
if (isLightControlLocked()) {
setLightsLockedOn(false, { source: 'modeGateReset' }).catch((err) => {
logger.warn('Failed to disable lights lock on mode change', err.message);
});
@@ -554,7 +559,7 @@ io.on('connection', (socket) => {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isLightControlLocked()) {
return cb({ error: 'Room controls are locked on' });
return cb({ error: 'Room controls are locked' });
}
try {
if (!entityId) throw new Error('entityId required');
@@ -574,7 +579,7 @@ io.on('connection', (socket) => {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isLightControlLocked()) {
return cb({ error: 'Room controls are locked on' });
return cb({ error: 'Room controls are locked' });
}
try {
@@ -595,7 +600,7 @@ io.on('connection', (socket) => {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isLightControlLocked()) {
return cb({ error: 'Room controls are locked on' });
return cb({ error: 'Room controls are locked' });
}
try {