Skip to content

Commit 9da8b60

Browse files
Merge pull request #299 from shannonhochkins/development
Development
2 parents 097c7d1 + f79e280 commit 9da8b60

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+38363
-12236
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
# 6.0.1
2+
3+
### General
4+
CHORE - Locales refreshed to the latest Home Assistant strings/hashes; locale key types updated for new tokens (e.g., any/first/last, additional device status terms).
5+
6+
### @hakit/core
7+
BUGFIX - HassConnect now guards all window access in suspend/resume and connection flows to avoid SSR/test crashes and ensure inherited connections/auth-callback cleanup stay safe (nextjs fix)
8+
IMPROVEMENT - Entity registry list derives domain titles via computeDomainTitle, improving labels/search without relying on locale strings.
9+
IMPROVEMENT - Service schema/types regenerated from the latest HA: frontend.setTheme supports optional light/dark names, scene services add apply/create/delete with transitions, media/recorder constraints refreshed.
10+
IMPROVEMENT - Humidifier entities expose target_humidity_step for finer control.
11+
12+
### @hakit/components
13+
No component code changes; version bumped to align with core 6.0.1.
14+
15+
### create-hakit
16+
Bump to 1.2.3 to consume the latest packages.
17+
118
# 6.0.0
219

320
## Migration Checklist

packages/components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@hakit/components",
33
"type": "module",
4-
"version": "6.0.0",
4+
"version": "6.0.1",
55
"private": false,
66
"keywords": [
77
"react",
@@ -70,7 +70,7 @@
7070
"@emotion/react": ">=11.x.x",
7171
"@emotion/styled": ">=11.x",
7272
"@fullcalendar/react": ">=6.x.x",
73-
"@hakit/core": "^6.0.0",
73+
"@hakit/core": "^6.0.1",
7474
"@use-gesture/react": ">=10.x",
7575
"autolinker": ">=4.x",
7676
"fullcalendar": ">=6.x.x",

packages/components/src/Shared/Modal/ModalByEntityDomain/index.tsx

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export type ModalByEntityDomainProps<E extends EntityName> = ModalPropsHelper<Ex
9494
hideUpdated?: boolean;
9595
hideAttributes?: boolean;
9696
hideLogbook?: boolean;
97+
hideDeviceSettings?: boolean;
9798
stateTitle?: ReactNode;
9899
/** There's currently a few default header actions, this will allow you to place your own actions in a different order @default 'start' */
99100
headerActionsPosition?: "start" | "middle" | "end";
@@ -124,6 +125,7 @@ export function ModalByEntityDomain<E extends EntityName>({
124125
hideState,
125126
hideUpdated,
126127
hideAttributes,
128+
hideDeviceSettings,
127129
headerActionsPosition = "start",
128130
headerActions,
129131
hideLogbook = false,
@@ -159,17 +161,20 @@ export function ModalByEntityDomain<E extends EntityName>({
159161
childProps,
160162
];
161163
}, [rest]);
164+
162165
const domain = computeDomain(entity);
163166

164167
const onStateChange = useCallback((value: string) => {
165168
if (!stateRef.current) return;
166169
stateRef.current.innerText = value;
167170
}, []);
171+
168172
const LazyModalComponent = useMemo(() => {
169173
const modal = getLazyModal(domain as keyof ModalPropsByDomain);
170174
if (!modal) return null;
171175
return lazy(modal);
172176
}, [domain]);
177+
173178
const defaultChildren = useMemo(() => {
174179
if (!LazyModalComponent || hideDefaultLayout) return null;
175180
const fallback = (
@@ -207,45 +212,55 @@ export function ModalByEntityDomain<E extends EntityName>({
207212
const titleValue = useMemo(() => {
208213
return modalProps.stateTitle ?? startCase(lowerCase(`${_entity.state}${_entity.attributes.unit_of_measurement ?? ""}`));
209214
}, [_entity, modalProps.stateTitle]);
215+
210216
return (
211217
<Modal
212218
{...modalProps}
213-
headerActions={() => {
214-
return (
215-
<>
216-
{!hideLogbook && showLogbook && (
217-
<FabCard
218-
title={localize("device")}
219-
tooltipPlacement="left"
220-
icon="mdi:arrow-back"
221-
size={30}
222-
onClick={() => setShowLogbook(false)}
223-
/>
224-
)}
225-
{headerActionsPosition === "start" && headerActions && headerActions()}
226-
{!hideLogbook && !showLogbook && (
227-
<FabCard
228-
title={localize("activity")}
229-
tooltipPlacement="left"
230-
icon="mdi:graph-box"
231-
size={30}
232-
onClick={() => setShowLogbook(true)}
233-
/>
234-
)}
235-
{headerActionsPosition === "middle" && headerActions && headerActions()}
236-
{device && device.device_id && (
237-
<FabCard title={localize("open_device_settings")} tooltipPlacement="left" icon="mdi:cog" size={30} onClick={openDevice} />
238-
)}
239-
{headerActionsPosition === "end" && headerActions && headerActions()}
240-
{(!hideLogbook || (device && device.device_id)) && <Separator />}
241-
</>
242-
);
243-
}}
244-
>
245-
{showLogbook ? (
219+
headerActions={() => (
246220
<>
247-
<LogBookRenderer entity={entity} />
221+
{/* Logbook Back Button */}
222+
{!hideLogbook && showLogbook && (
223+
<FabCard
224+
title={localize("device")}
225+
tooltipPlacement="left"
226+
icon="mdi:arrow-back"
227+
size={30}
228+
onClick={() => setShowLogbook(false)}
229+
/>
230+
)}
231+
232+
{/* Header actions at the start */}
233+
{headerActionsPosition === "start" && headerActions && headerActions()}
234+
235+
{/* Logbook Open Button */}
236+
{!hideLogbook && !showLogbook && (
237+
<FabCard
238+
title={localize("activity")}
239+
tooltipPlacement="left"
240+
icon="mdi:graph-box"
241+
size={30}
242+
onClick={() => setShowLogbook(true)}
243+
/>
244+
)}
245+
246+
{/* Header actions in the middle */}
247+
{headerActionsPosition === "middle" && headerActions && headerActions()}
248+
249+
{/* Device Settings (Cog) */}
250+
{device && device.device_id && !hideDeviceSettings && (
251+
<FabCard title={localize("open_device_settings")} tooltipPlacement="left" icon="mdi:cog" size={30} onClick={openDevice} />
252+
)}
253+
254+
{/* Header actions at the end */}
255+
{headerActionsPosition === "end" && headerActions && headerActions()}
256+
257+
{/* Separator if either logbook or settings exists */}
258+
{(!hideLogbook || (device && device.device_id && !hideDeviceSettings)) && <Separator />}
248259
</>
260+
)}
261+
>
262+
{showLogbook ? (
263+
<LogBookRenderer entity={entity} />
249264
) : (
250265
<>
251266
{(!hideUpdated || !hideState) && (

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hakit/core",
3-
"version": "6.0.0",
3+
"version": "6.0.1",
44
"private": false,
55
"type": "module",
66
"keywords": [

packages/core/src/HassConnect/Provider.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,9 @@ export function HassProvider({
255255
}, [_hash]);
256256

257257
useEffect(() => {
258-
window.addEventListener("hashchange", onHashChange);
258+
if (typeof window !== "undefined") window.addEventListener("hashchange", onHashChange);
259259
return () => {
260-
window.removeEventListener("hashchange", onHashChange);
260+
if (typeof window !== "undefined") window.removeEventListener("hashchange", onHashChange);
261261
};
262262
}, []);
263263

packages/core/src/HassConnect/handleSuspendResume.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,24 +99,27 @@ export function handleSuspendResume(connection: Connection, options: HandleSuspe
9999

100100
// Wait DELAY_MS before actually closing the socket.
101101
if (debug) console.log(`[SR] Starting hidden delay of ${DELAY_MS}ms before actual suspend()`);
102-
hiddenTimeoutId = window.setTimeout(() => {
103-
hiddenTimeoutId = null;
104-
// If still hidden, suspend the connection:
105-
if (document.hidden) {
106-
if (debug) console.log("[SR] Hidden timeout elapsed → calling suspend()");
107-
suspend();
108-
} else {
109-
// User returned before timeout. Resolve immediately.
110-
if (pendingResolve) {
111-
if (debug) console.log("[SR] Hidden timeout elapsed but page is visible → resolving pendingResolve()");
112-
// potentially unreachable, but just in case!
113-
pendingResolve();
114-
}
115-
}
116-
}, DELAY_MS);
102+
hiddenTimeoutId =
103+
typeof window !== "undefined"
104+
? window.setTimeout(() => {
105+
hiddenTimeoutId = null;
106+
// If still hidden, suspend the connection:
107+
if (document.hidden) {
108+
if (debug) console.log("[SR] Hidden timeout elapsed → calling suspend()");
109+
suspend();
110+
} else {
111+
// User returned before timeout. Resolve immediately.
112+
if (pendingResolve) {
113+
if (debug) console.log("[SR] Hidden timeout elapsed but page is visible → resolving pendingResolve()");
114+
// potentially unreachable, but just in case!
115+
pendingResolve();
116+
}
117+
}
118+
}, DELAY_MS)
119+
: null;
117120

118121
// If the user focuses before DELAY_MS is up, resume immediately:
119-
window.addEventListener("focus", onVisibleOrResume, { once: true });
122+
if (typeof window !== "undefined") window.addEventListener("focus", onVisibleOrResume, { once: true });
120123
}
121124

122125
// helper when page/tab becomes visible or “resume” fires after freeze
@@ -161,7 +164,7 @@ export function handleSuspendResume(connection: Connection, options: HandleSuspe
161164
}
162165
onStatusChange?.("suspended");
163166
if (debug) console.log("[SR] suspend() called → suspending connection");
164-
window.stop();
167+
if (typeof window !== "undefined") window.stop();
165168
connection.suspend();
166169
}
167170

@@ -179,7 +182,7 @@ export function handleSuspendResume(connection: Connection, options: HandleSuspe
179182
document.removeEventListener("visibilitychange", visibilityChangeHandler, false);
180183
document.removeEventListener("freeze", suspend);
181184
document.removeEventListener("resume", resumeHandler);
182-
window.removeEventListener("focus", onVisibleOrResume);
185+
if (typeof window !== "undefined") window.removeEventListener("focus", onVisibleOrResume);
183186

184187
if (hiddenTimeoutId !== null) {
185188
if (debug) console.log("[SR] cleanup: Clearing hiddenTimeoutId");

packages/core/src/HassConnect/tryConnection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ type ConnectionType = "auth-callback" | "user-request" | "saved-tokens" | "inher
6161

6262
function getInheritedConnection(): typeof window.hassConnection | undefined {
6363
try {
64-
return window.top?.hassConnection;
64+
return typeof window !== "undefined" ? window.top?.hassConnection : undefined;
6565
} catch (e) {
6666
console.error("Error getting inherited connection", e);
6767
return undefined;
@@ -185,7 +185,7 @@ export const tryConnection = async (hassUrl: string, hassToken?: string): Promis
185185
};
186186
} finally {
187187
// Clear url if we have a auth callback in url.
188-
if (location && location.search.includes("auth_callback=1")) {
188+
if (typeof window !== "undefined" && location && location.search.includes("auth_callback=1")) {
189189
history.replaceState(null, "", location.pathname);
190190
}
191191
}

packages/core/src/hooks/useEntity/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function useEntity<E extends EntityName, O extends UseEntityOptions = Use
7777
// if returnNullIfNotFound is true, we return null, if not we throw an error
7878
return null as unknown as UseEntityReturnType<E, O>;
7979
}
80-
return { ...formatted, service, history } as UseEntityReturnType<E, O>;
80+
return { ...formatted, service, history } as unknown as UseEntityReturnType<E, O>;
8181
}, [formatted, service, history]);
8282

8383
return entityWithHelpers;

0 commit comments

Comments
 (0)