mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
home assistant white light buttons
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
You are The Overseer of the rovers. You are able to see their actions and the chatting of the people using them. You are not able to control them.
|
You are The Overseer of the rovers. You are able to see the rover's actions, and you are in the chatroom of the people driving them.
|
||||||
|
You are not able to control the people or the rovers.
|
||||||
Your purpose is to provide entertainment and break the silence.
|
|
||||||
|
|
||||||
Output contract:
|
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.
|
||||||
|
|||||||
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
@@ -11,8 +11,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Cxe8USDb.js"></script>
|
<script type="module" crossorigin src="/assets/index-BrQIm3tI.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BquPV0pG.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-gFnxuq00.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const triggerConfig = []; // [{ runtimeKey, entityId, action, stateEquals, paylo
|
|||||||
const triggerRuntime = new Map(); // triggerId -> { lastFiredAt, lastState, lastChanged, lastUpdated }
|
const triggerRuntime = new Map(); // triggerId -> { lastFiredAt, lastState, lastChanged, lastUpdated }
|
||||||
const HA_BUTTON_EVENT_TYPE = 'ha.button.action';
|
const HA_BUTTON_EVENT_TYPE = 'ha.button.action';
|
||||||
const LIGHT_IDLE_OFF_MS = 2 * 60 * 1000;
|
const LIGHT_IDLE_OFF_MS = 2 * 60 * 1000;
|
||||||
|
const DEFAULT_WHITE_KELVIN = 4000;
|
||||||
|
|
||||||
let connection = null;
|
let connection = null;
|
||||||
let unsubscribeEntities = null;
|
let unsubscribeEntities = null;
|
||||||
@@ -463,6 +464,31 @@ async function setLightColor(entityId, rgbColor) {
|
|||||||
logger.info('Issued Home Assistant color command', { entityId, rgbColor: normalized });
|
logger.info('Issued Home Assistant color command', { entityId, rgbColor: normalized });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setLightWhite(entityId, kelvin = DEFAULT_WHITE_KELVIN) {
|
||||||
|
if (!enabled) {
|
||||||
|
throw new Error('Home Assistant not configured');
|
||||||
|
}
|
||||||
|
const meta = entityConfig.get(entityId);
|
||||||
|
if (!meta || meta.type !== 'light') {
|
||||||
|
throw new Error('Home Assistant light required');
|
||||||
|
}
|
||||||
|
if (!connection) {
|
||||||
|
throw new Error('Home Assistant not connected');
|
||||||
|
}
|
||||||
|
const nextKelvin = Number(kelvin);
|
||||||
|
const normalizedKelvin = Number.isFinite(nextKelvin)
|
||||||
|
? Math.max(2000, Math.min(6500, Math.round(nextKelvin)))
|
||||||
|
: DEFAULT_WHITE_KELVIN;
|
||||||
|
await callService(connection, 'light', 'turn_on', {
|
||||||
|
entity_id: entityId,
|
||||||
|
color_temp_kelvin: normalizedKelvin,
|
||||||
|
});
|
||||||
|
logger.info('Issued Home Assistant white command', {
|
||||||
|
entityId,
|
||||||
|
colorTempKelvin: normalizedKelvin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function isLightControlLocked() {
|
function isLightControlLocked() {
|
||||||
return lightsLockState != null;
|
return lightsLockState != null;
|
||||||
}
|
}
|
||||||
@@ -611,6 +637,27 @@ io.on('connection', (socket) => {
|
|||||||
cb({ error: err.message });
|
cb({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('homeAssistant:lightWhite', async ({ entityId } = {}, cb = () => {}) => {
|
||||||
|
const mode = getMode();
|
||||||
|
if (
|
||||||
|
(mode === 'admin' && isAdmin(socket) !== true) ||
|
||||||
|
(mode === 'lockdown' && isLockdownAdmin(socket) !== true)
|
||||||
|
) {
|
||||||
|
return cb({ error: 'Insufficient permissions to control Home Assistant' });
|
||||||
|
}
|
||||||
|
if (isLightControlLocked()) {
|
||||||
|
return cb({ error: 'Room controls are locked' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!entityId) throw new Error('entityId required');
|
||||||
|
await setLightWhite(entityId, haConfig?.whiteKelvin);
|
||||||
|
cb({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
cb({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -620,6 +667,7 @@ module.exports = {
|
|||||||
toggleEntity,
|
toggleEntity,
|
||||||
setEntityState,
|
setEntityState,
|
||||||
setLightColor,
|
setLightColor,
|
||||||
|
setLightWhite,
|
||||||
setLightsLockedOn,
|
setLightsLockedOn,
|
||||||
toggleLightsLockedOn,
|
toggleLightsLockedOn,
|
||||||
homeAssistantEvents: events,
|
homeAssistantEvents: events,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function getEntityHue(entity) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EntityRow({ entity, connected, controlsLocked, onToggle, onSetColor }) {
|
function EntityRow({ entity, connected, controlsLocked, onToggle, onSetColor, onSetWhite }) {
|
||||||
const unavailable = entity.state === 'unavailable' || !entity.available;
|
const unavailable = entity.state === 'unavailable' || !entity.available;
|
||||||
const isOn = entity.state === 'on';
|
const isOn = entity.state === 'on';
|
||||||
const supportsColor = entity.type === 'light' && entity.supportsColor;
|
const supportsColor = entity.type === 'light' && entity.supportsColor;
|
||||||
@@ -125,6 +125,12 @@ function EntityRow({ entity, connected, controlsLocked, onToggle, onSetColor })
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSetWhite = (event) => {
|
||||||
|
stopPropagation(event);
|
||||||
|
if (disableColor || !onSetWhite) return;
|
||||||
|
onSetWhite(entity.id);
|
||||||
|
};
|
||||||
|
|
||||||
const displayRgb = supportsColor ? hueToRgb(hueRef.current) : [255, 255, 255];
|
const displayRgb = supportsColor ? hueToRgb(hueRef.current) : [255, 255, 255];
|
||||||
const displayColor = `rgb(${displayRgb.join(',')})`;
|
const displayColor = `rgb(${displayRgb.join(',')})`;
|
||||||
|
|
||||||
@@ -191,10 +197,35 @@ function EntityRow({ entity, connected, controlsLocked, onToggle, onSetColor })
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{supportsColor && (
|
{supportsColor && (
|
||||||
<span
|
<div className="absolute right-1 top-1 flex items-center gap-0.5">
|
||||||
className="pointer-events-none absolute right-1 top-1 h-2.5 w-2.5 rounded-full border border-white/60"
|
<span
|
||||||
style={{ backgroundColor: displayColor }}
|
role="button"
|
||||||
/>
|
tabIndex={disableColor ? -1 : 0}
|
||||||
|
onClick={handleSetWhite}
|
||||||
|
onPointerDown={stopPropagation}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (disableColor) return;
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleSetWhite(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Set white color temperature"
|
||||||
|
aria-label={`Set ${entity.name || entity.id} to white`}
|
||||||
|
aria-disabled={disableColor}
|
||||||
|
className={`inline-flex items-center justify-center rounded border border-slate-200 bg-white px-1 py-0.5 text-xs font-semibold leading-none text-slate-900 ${
|
||||||
|
disableColor
|
||||||
|
? 'cursor-not-allowed opacity-60'
|
||||||
|
: 'cursor-pointer hover:bg-slate-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
white
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="pointer-events-none h-2.5 w-2.5 rounded-full border border-white/60"
|
||||||
|
style={{ backgroundColor: displayColor }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{!supportsColor && (
|
{!supportsColor && (
|
||||||
<div className="self-center text-xs font-semibold text-white/90">
|
<div className="self-center text-xs font-semibold text-white/90">
|
||||||
@@ -209,7 +240,8 @@ export default function HomeAssistantControls() {
|
|||||||
const {
|
const {
|
||||||
state: { keymap },
|
state: { keymap },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const { session, homeAssistantToggle, homeAssistantSetLightColor } = useSession();
|
const { session, homeAssistantToggle, homeAssistantSetLightColor, homeAssistantSetLightWhite } =
|
||||||
|
useSession();
|
||||||
const ha = session?.homeAssistant;
|
const ha = session?.homeAssistant;
|
||||||
const entities = useMemo(() => ha?.entities || [], [ha?.entities]);
|
const entities = useMemo(() => ha?.entities || [], [ha?.entities]);
|
||||||
const lightPolicy = ha?.lightPolicy || null;
|
const lightPolicy = ha?.lightPolicy || null;
|
||||||
@@ -274,6 +306,7 @@ export default function HomeAssistantControls() {
|
|||||||
controlsLocked={controlsLocked}
|
controlsLocked={controlsLocked}
|
||||||
onToggle={homeAssistantToggle}
|
onToggle={homeAssistantToggle}
|
||||||
onSetColor={homeAssistantSetLightColor}
|
onSetColor={homeAssistantSetLightColor}
|
||||||
|
onSetWhite={homeAssistantSetLightWhite}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const SessionContext = createContext({
|
|||||||
homeAssistantToggle: async () => {},
|
homeAssistantToggle: async () => {},
|
||||||
homeAssistantSetState: async () => {},
|
homeAssistantSetState: async () => {},
|
||||||
homeAssistantSetLightColor: async () => {},
|
homeAssistantSetLightColor: async () => {},
|
||||||
|
homeAssistantSetLightWhite: async () => {},
|
||||||
setNickname: async () => {},
|
setNickname: async () => {},
|
||||||
requestVerification: async () => {},
|
requestVerification: async () => {},
|
||||||
requestPrivateRoverAccess: async () => {},
|
requestPrivateRoverAccess: async () => {},
|
||||||
@@ -141,6 +142,8 @@ export function SessionProvider({ children }) {
|
|||||||
emitWithAck('homeAssistant:setState', { entityId, state }),
|
emitWithAck('homeAssistant:setState', { entityId, state }),
|
||||||
homeAssistantSetLightColor: (entityId, rgbColor) =>
|
homeAssistantSetLightColor: (entityId, rgbColor) =>
|
||||||
emitWithAck('homeAssistant:lightColor', { entityId, rgbColor }),
|
emitWithAck('homeAssistant:lightColor', { entityId, rgbColor }),
|
||||||
|
homeAssistantSetLightWhite: (entityId) =>
|
||||||
|
emitWithAck('homeAssistant:lightWhite', { entityId }),
|
||||||
setNickname: (nickname) => emitWithAck('nickname:set', { nickname }),
|
setNickname: (nickname) => emitWithAck('nickname:set', { nickname }),
|
||||||
requestVerification: () => emitWithAck('verification:request'),
|
requestVerification: () => emitWithAck('verification:request'),
|
||||||
requestPrivateRoverAccess: (roverId) =>
|
requestPrivateRoverAccess: (roverId) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user