mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
making noise
This commit is contained in:
@@ -78,9 +78,36 @@ function hasEntity(entityIdValue) {
|
|||||||
return Boolean(readRaw(entityIdValue));
|
return Boolean(readRaw(entityIdValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEntityAvailable(entityIdValue) {
|
||||||
|
const raw = readRaw(entityIdValue);
|
||||||
|
if (!raw) return false;
|
||||||
|
const state = String(raw.state ?? '').trim().toLowerCase();
|
||||||
|
if (!state) return false;
|
||||||
|
return state !== 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredEntityIds() {
|
||||||
|
return [
|
||||||
|
ENTITY_IDS.buttons.start,
|
||||||
|
ENTITY_IDS.buttons.sendHome,
|
||||||
|
ENTITY_IDS.buttons.locate,
|
||||||
|
ENTITY_IDS.buttons.clearErrors,
|
||||||
|
ENTITY_IDS.sensors.batteryPercent,
|
||||||
|
ENTITY_IDS.sensors.batteryVoltage,
|
||||||
|
ENTITY_IDS.binarySensors.chargingActive,
|
||||||
|
ENTITY_IDS.binarySensors.extPowerPresent,
|
||||||
|
ENTITY_IDS.textSensors.uiState,
|
||||||
|
ENTITY_IDS.textSensors.robotError,
|
||||||
|
ENTITY_IDS.textSensors.robotAlert,
|
||||||
|
].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
function buildState() {
|
function buildState() {
|
||||||
const configured = Boolean(device);
|
const configured = Boolean(device);
|
||||||
const connected = isHomeAssistantConnected();
|
const haConnected = isHomeAssistantConnected();
|
||||||
|
const requiredIds = requiredEntityIds();
|
||||||
|
const entitiesAvailable = requiredIds.length > 0 && requiredIds.every((id) => isEntityAvailable(id));
|
||||||
|
const connected = Boolean(haConnected && entitiesAvailable);
|
||||||
const enabled = Boolean(homeAssistantEnabled && configured);
|
const enabled = Boolean(homeAssistantEnabled && configured);
|
||||||
|
|
||||||
const controls = {
|
const controls = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Vip Neato Card
|
// Vip Neato Card
|
||||||
// Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file.
|
// Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file.
|
||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useMemo, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
function normalizeState(value) {
|
function normalizeState(value) {
|
||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
@@ -56,7 +56,6 @@ export default function VipNeatoCard({
|
|||||||
const charging = Boolean(neato?.telemetry?.chargingActive);
|
const charging = Boolean(neato?.telemetry?.chargingActive);
|
||||||
|
|
||||||
const uiStateLabel = humanizeUiState(neato?.telemetry?.uiState);
|
const uiStateLabel = humanizeUiState(neato?.telemetry?.uiState);
|
||||||
const uiStateRaw = normalizeState(neato?.telemetry?.uiState).toUpperCase();
|
|
||||||
const battery = neato?.telemetry?.batteryPercent;
|
const battery = neato?.telemetry?.batteryPercent;
|
||||||
const batteryLabel = Number.isFinite(battery) ? `${battery}%` : '--';
|
const batteryLabel = Number.isFinite(battery) ? `${battery}%` : '--';
|
||||||
const voltage = neato?.telemetry?.batteryVoltage;
|
const voltage = neato?.telemetry?.batteryVoltage;
|
||||||
@@ -73,29 +72,8 @@ export default function VipNeatoCard({
|
|||||||
const canLocate = Boolean(controls?.locate?.available);
|
const canLocate = Boolean(controls?.locate?.available);
|
||||||
const canClearErrors = Boolean(controls?.clearErrors?.available);
|
const canClearErrors = Boolean(controls?.clearErrors?.available);
|
||||||
|
|
||||||
const primaryAction = useMemo(() => {
|
const canRunStart = configured && connected && canStart;
|
||||||
const looksDockedByState = uiStateRaw.includes('STATE_IDLE') || uiStateRaw.includes('STATE_STANDBY');
|
const canRunSendHome = configured && connected && canSendHome;
|
||||||
if (docked || looksDockedByState) {
|
|
||||||
return {
|
|
||||||
key: 'start',
|
|
||||||
label: 'Start cleaning',
|
|
||||||
pending: 'Starting...',
|
|
||||||
toneClass: 'bg-emerald-600 hover:bg-emerald-500 text-white',
|
|
||||||
canRun: canStart,
|
|
||||||
fn: onStart,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
key: 'home',
|
|
||||||
label: 'Send to dock',
|
|
||||||
pending: 'Sending...',
|
|
||||||
toneClass: 'bg-sky-600 hover:bg-sky-500 text-white',
|
|
||||||
canRun: canSendHome,
|
|
||||||
fn: onSendHome,
|
|
||||||
};
|
|
||||||
}, [docked, uiStateRaw, canStart, onStart, canSendHome, onSendHome]);
|
|
||||||
|
|
||||||
const canRunPrimary = configured && connected && primaryAction.canRun;
|
|
||||||
const canRunLocate = configured && connected && canLocate;
|
const canRunLocate = configured && connected && canLocate;
|
||||||
const canRunClearErrors = configured && connected && canClearErrors;
|
const canRunClearErrors = configured && connected && canClearErrors;
|
||||||
|
|
||||||
@@ -125,7 +103,7 @@ export default function VipNeatoCard({
|
|||||||
<div className="relative flex items-center justify-center min-h-[1.5rem]">
|
<div className="relative flex items-center justify-center min-h-[1.5rem]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-slate-100">Neato Controls</p>
|
<p className="text-sm text-slate-100">Neato Controls</p>
|
||||||
<p className="text-xs text-slate-400">Control the autonomous Neato robovac</p>
|
<p className="text-xs text-slate-400">Control the autonomous Neato robovac. Be nice to him.</p>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`absolute right-0 inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${metricToneClass(headerTone)}`}
|
className={`absolute right-0 inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${metricToneClass(headerTone)}`}
|
||||||
@@ -135,37 +113,49 @@ export default function VipNeatoCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="surface-muted px-0.5 py-0.5">
|
<section className="surface-muted px-0.5 py-0.5">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-0.5">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-0.5">
|
||||||
<div className="grid gap-0.5">
|
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center">
|
||||||
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center">
|
<div className="text-xs text-slate-300">State</div>
|
||||||
<div className="text-xs text-slate-300">State</div>
|
<div className="text-base font-semibold text-slate-100">{primaryState}</div>
|
||||||
<div className="text-base font-semibold text-slate-100">{primaryState}</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!canRunPrimary || Boolean(working)}
|
|
||||||
onClick={() => runAction(primaryAction.key, primaryAction.fn)}
|
|
||||||
className={`rounded-md px-1 py-1 text-base font-semibold transition disabled:opacity-50 ${primaryAction.toneClass}`}
|
|
||||||
>
|
|
||||||
{working === primaryAction.key ? primaryAction.pending : primaryAction.label}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!canRunLocate || Boolean(working)}
|
disabled={!canRunStart || Boolean(working)}
|
||||||
onClick={() => runAction('locate', onLocate)}
|
onClick={() => runAction('start', onStart)}
|
||||||
className="rounded-md bg-fuchsia-600 px-1 py-0.75 text-sm font-semibold text-white transition hover:bg-fuchsia-500 disabled:opacity-50"
|
className="rounded-md border border-sky-300 bg-emerald-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-emerald-500 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{working === 'locate' ? 'Playing...' : 'Play sound'}
|
{working === 'start' ? 'Starting...' : 'Start cleaning'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!canRunClearErrors || Boolean(working)}
|
disabled={!canRunSendHome || Boolean(working)}
|
||||||
onClick={() => runAction('clearErrors', onClearErrors)}
|
onClick={() => runAction('sendHome', onSendHome)}
|
||||||
className="rounded-md bg-amber-500 px-1 py-0.75 text-sm font-semibold text-slate-900 transition hover:bg-amber-400 disabled:opacity-50"
|
className="rounded-md border border-sky-300 bg-sky-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-sky-500 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{working === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
|
{working === 'sendHome' ? 'Sending...' : 'Send to dock'}
|
||||||
</button>
|
</button>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-1 gap-0.5">
|
||||||
|
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!canRunLocate || Boolean(working)}
|
||||||
|
onClick={() => runAction('locate', onLocate)}
|
||||||
|
className="w-full rounded-md border border-sky-300 bg-fuchsia-600 px-1 py-0.5 text-xs font-semibold text-white transition hover:border-sky-500 hover:bg-fuchsia-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{working === 'locate' ? 'Playing...' : 'Play sound'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!canRunClearErrors || Boolean(working)}
|
||||||
|
onClick={() => runAction('clearErrors', onClearErrors)}
|
||||||
|
className="w-full rounded-md border border-sky-300 bg-amber-500 px-1 py-0.5 text-xs font-semibold text-slate-900 transition hover:border-sky-500 hover:bg-amber-400 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{working === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -175,7 +165,12 @@ export default function VipNeatoCard({
|
|||||||
<StatusTile label="Voltage" value={voltageLabel} tone={voltage == null ? 'muted' : 'info'} />
|
<StatusTile label="Voltage" value={voltageLabel} tone={voltage == null ? 'muted' : 'info'} />
|
||||||
<StatusTile label="Docked" value={docked ? 'Yes' : 'No'} tone={docked ? 'info' : 'muted'} />
|
<StatusTile label="Docked" value={docked ? 'Yes' : 'No'} tone={docked ? 'info' : 'muted'} />
|
||||||
<StatusTile label="Charging" value={charging ? 'Yes' : 'No'} tone={charging ? 'info' : 'muted'} />
|
<StatusTile label="Charging" value={charging ? 'Yes' : 'No'} tone={charging ? 'info' : 'muted'} />
|
||||||
<StatusTile label="UI state" value={uiStateLabel} tone="muted" valueClass="truncate" />
|
<StatusTile
|
||||||
|
label="UI state"
|
||||||
|
value={uiStateLabel}
|
||||||
|
tone="muted"
|
||||||
|
valueClass="truncate"
|
||||||
|
/>
|
||||||
<StatusTile
|
<StatusTile
|
||||||
label="Robot error"
|
label="Robot error"
|
||||||
value={robotError}
|
value={robotError}
|
||||||
@@ -191,7 +186,7 @@ export default function VipNeatoCard({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{!configured || !connected || !primaryAction.canRun || !canLocate || !canClearErrors ? (
|
{!configured || !connected || !canStart || !canSendHome || !canLocate || !canClearErrors ? (
|
||||||
<p className="text-xs text-slate-400 text-center">
|
<p className="text-xs text-slate-400 text-center">
|
||||||
{!configured
|
{!configured
|
||||||
? 'Set homeAssistant.neato.device in server config to enable Neato controls.'
|
? 'Set homeAssistant.neato.device in server config to enable Neato controls.'
|
||||||
|
|||||||
Reference in New Issue
Block a user