IR controls

This commit is contained in:
legop3
2026-07-11 03:59:34 -04:00
parent 10121650de
commit f2d3567978
6 changed files with 51 additions and 13 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -78,7 +78,7 @@
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script> <script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script> <script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CWTcRRlU.js"></script> <script type="module" crossorigin src="/assets/index-DC_FLGBQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DwpUkSbG.css"> <link rel="stylesheet" crossorigin href="/assets/index-DwpUkSbG.css">
</head> </head>
<body> <body>
+21 -3
View File
@@ -125,6 +125,18 @@ function normalizeSpotlightPayloadState(rawState) {
return Boolean(Number(rawState)); return Boolean(Number(rawState));
} }
function normalizeIrState(rawState) {
/*
Reolink accepts exactly Auto, On, and Off for this camera's IR LED control.
Normalize UI payloads at the server boundary so keyboard, mobile, and any
future direct socket callers all hit the same camera API contract.
*/
const normalized = String(rawState || '').trim().toLowerCase();
if (normalized === 'on' || normalized === '1' || normalized === 'true') return 'On';
if (normalized === 'off' || normalized === '0' || normalized === 'false') return 'Off';
return 'Auto';
}
function passesMode(socket) { function passesMode(socket) {
const mode = getMode(); const mode = getMode();
if (mode === MODES.LOCKDOWN) return isLockdownAdmin(socket); if (mode === MODES.LOCKDOWN) return isLockdownAdmin(socket);
@@ -650,11 +662,17 @@ async function setSpotlight(socket, payload = {}) {
async function setIr(socket, payload = {}) { async function setIr(socket, payload = {}) {
requireOperator(socket); requireOperator(socket);
return serializeVendorState(async () => { return serializeVendorState(async () => {
const nextState = String(payload.state || '').toLowerCase() === 'off' ? 'Off' : 'Auto'; const nextState = normalizeIrState(payload.state);
const client = await ensureReolinkClient(); const client = await ensureReolinkClient();
state.ir = { ...(state.ir || {}), state: nextState }; /*
The camera requires channel inside IrLights. Without it, SetIrLights
returns param error (-4), while the optimistic local state makes the UI
look like the command worked. Keep the optimistic state, but send the
minimal payload the camera actually accepts.
*/
state.ir = { ...(state.ir || {}), channel: 0, state: nextState };
emitChange('ir-pending'); emitChange('ir-pending');
await client.api('SetIrLights', { IrLights: { state: nextState } }); await client.api('SetIrLights', { IrLights: { channel: 0, state: nextState } });
await refreshVendorState(); await refreshVendorState();
emitChange('ir'); emitChange('ir');
return state.ir; return state.ir;
+22 -2
View File
@@ -77,6 +77,25 @@ function isSpotlightOn(light = {}) {
return Boolean(Number(raw)); return Boolean(Number(raw));
} }
function normalizeIrMode(mode) {
const normalized = String(mode || '').trim().toLowerCase();
if (normalized === 'on') return 'On';
if (normalized === 'off') return 'Off';
return 'Auto';
}
function nextIrMode(currentMode) {
/*
The rover laser button becomes the PTZ camera's IR mode control while the
PTZ camera is active. Cycle all three modes exposed by this Reolink camera
instead of reducing the control to a two-state toggle.
*/
const current = normalizeIrMode(currentMode);
if (current === 'Auto') return 'On';
if (current === 'On') return 'Off';
return 'Auto';
}
export function usePtzControlAdapter() { export function usePtzControlAdapter() {
const socket = useSocket(); const socket = useSocket();
const ptz = useSessionSelector((state) => state.session?.ptzCamera || null); const ptz = useSessionSelector((state) => state.session?.ptzCamera || null);
@@ -176,8 +195,9 @@ export function usePtzControlAdapter() {
const setIr = useCallback( const setIr = useCallback(
(nextOn) => { (nextOn) => {
if (!isActive) return false; if (!isActive) return false;
const currentOff = String(ptz?.ir?.state || '').toLowerCase() === 'off'; const desiredState = typeof nextOn === 'boolean'
const desiredState = typeof nextOn === 'boolean' ? (nextOn ? 'Auto' : 'Off') : currentOff ? 'Auto' : 'Off'; ? (nextOn ? 'On' : 'Off')
: nextIrMode(ptz?.ir?.state);
emitPtz('ptzCamera:ir', { state: desiredState }); emitPtz('ptzCamera:ir', { state: desiredState });
return true; return true;
}, },