Merge pull request 'Release: admin timezone setting (0.9.3) (2026-09-26)' (#267) from release/2026-09-26-admin-timezone into main
Reviewed-on: #267
This commit was merged in pull request #267.
This commit is contained in:
@@ -1,5 +1,34 @@
|
|||||||
# NestQuest Release Notes
|
# NestQuest Release Notes
|
||||||
|
|
||||||
|
## Version 0.9.3 — 2026-09-26
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
Patch release carrying the admin timezone control onto `main`.
|
||||||
|
|
||||||
|
- **Admins set the system timezone from the PWA:** the Preferences screen gains a
|
||||||
|
Timezone field (a text input with a local datalist of common IANA zones;
|
||||||
|
`UTC` or `Area/Location`). On a fresh install the value is set AUTOMATICALLY
|
||||||
|
from the browser's IANA zone when the setting is empty on first load — no
|
||||||
|
geolocation prompt and no network lookup — and an admin can change or clear it
|
||||||
|
at any time. Only a changed value is sent, and the API's 422 for an unknown zone
|
||||||
|
is shown by the form. (PR #266.)
|
||||||
|
- This makes the household-local clock (overdue, daily rollover) configurable
|
||||||
|
from the UI, replacing the manual
|
||||||
|
`PATCH /api/v1/admin/settings {"timezone": ...}` step noted in 0.9.1. The
|
||||||
|
setting still defaults to the API host's local time when left empty.
|
||||||
|
|
||||||
|
### Upgrade note
|
||||||
|
|
||||||
|
The database schema is unchanged at version **9**; no migration or backup step is
|
||||||
|
needed. No runtime dependency is added.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
Home Assistant 2024.6.0 or newer. Install through HACS from the
|
||||||
|
`Maniacs-Mansion/NestQuest` GitHub mirror (kept current by the Gitea push
|
||||||
|
mirror).
|
||||||
|
|
||||||
## Version 0.9.2 — 2026-09-26
|
## Version 0.9.2 — 2026-09-26
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface HouseholdSettings {
|
|||||||
afternoon_reminder_enabled: boolean;
|
afternoon_reminder_enabled: boolean;
|
||||||
end_of_day_report_enabled: boolean;
|
end_of_day_report_enabled: boolean;
|
||||||
celebration_enabled: boolean;
|
celebration_enabled: boolean;
|
||||||
|
/** IANA zone name (e.g. "America/New_York"); "" means the API host's local time. */
|
||||||
|
timezone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Only the supplied fields are updated; the API answers with all of them. */
|
/** Only the supplied fields are updated; the API answers with all of them. */
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import App from "../App";
|
import App from "../App";
|
||||||
@@ -21,6 +22,7 @@ const DEFAULT_SETTINGS: HouseholdSettings = {
|
|||||||
afternoon_reminder_enabled: false,
|
afternoon_reminder_enabled: false,
|
||||||
end_of_day_report_enabled: true,
|
end_of_day_report_enabled: true,
|
||||||
celebration_enabled: false,
|
celebration_enabled: false,
|
||||||
|
timezone: "America/Chicago",
|
||||||
};
|
};
|
||||||
|
|
||||||
function child(
|
function child(
|
||||||
@@ -370,10 +372,11 @@ describe("SettingsScreen — preferences", () => {
|
|||||||
fireEvent.click(within(form).getByRole("button", { name: "Save preferences" }));
|
fireEvent.click(within(form).getByRole("button", { name: "Save preferences" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
it("renders the current value of the ten settings form fields (timezone is API-only)", async () => {
|
it("renders the current value of all eleven settings form fields", async () => {
|
||||||
const { form } = await renderPreferences();
|
const { form } = await renderPreferences();
|
||||||
expect(field(form, "Planning horizon (days)").value).toBe("14");
|
expect(field(form, "Planning horizon (days)").value).toBe("14");
|
||||||
expect(field(form, "Day rollover").value).toBe("03:00");
|
expect(field(form, "Day rollover").value).toBe("03:00");
|
||||||
|
expect(field(form, "Timezone").value).toBe("America/Chicago");
|
||||||
expect(field(form, "Notify target").value).toBe("notify.family");
|
expect(field(form, "Notify target").value).toBe("notify.family");
|
||||||
expect(field(form, "Morning summary time").value).toBe("07:00");
|
expect(field(form, "Morning summary time").value).toBe("07:00");
|
||||||
expect(field(form, "Afternoon reminder time").value).toBe("15:30");
|
expect(field(form, "Afternoon reminder time").value).toBe("15:30");
|
||||||
@@ -496,6 +499,46 @@ describe("SettingsScreen — preferences", () => {
|
|||||||
expect(writes()).toHaveLength(1);
|
expect(writes()).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("a changed timezone patches exactly the timezone", async () => {
|
||||||
|
const { form } = await renderPreferences();
|
||||||
|
const input = field(form, "Timezone");
|
||||||
|
expect(input.getAttribute("list")).toBe("settings-timezones");
|
||||||
|
fireEvent.change(input, { target: { value: " Europe/London " } });
|
||||||
|
save(form);
|
||||||
|
await screen.findByText("Preferences saved.");
|
||||||
|
expect(writes()).toEqual([{ method: "PATCH", path: SETTINGS_PATH, body: { timezone: "Europe/London" } }]);
|
||||||
|
const reloaded = screen.getByRole("form", { name: "Preferences" });
|
||||||
|
expect(field(reloaded, "Timezone").value).toBe("Europe/London");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["New York", "America/New York", "EST5EDT?", "/Chicago"])(
|
||||||
|
"an invalid timezone %j is rejected before any request",
|
||||||
|
async (value) => {
|
||||||
|
const { form } = await renderPreferences();
|
||||||
|
fireEvent.change(field(form, "Timezone"), { target: { value } });
|
||||||
|
save(form);
|
||||||
|
expect(screen.getByTestId("preferences-message").textContent).toBe(
|
||||||
|
"Timezone must be an IANA zone like America/New_York, or UTC.",
|
||||||
|
);
|
||||||
|
expect(field(form, "Timezone").value).toBe(value);
|
||||||
|
expect(writes()).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("a 422 on an unknown timezone shows the API detail and keeps the edit", async () => {
|
||||||
|
const { form } = await renderPreferences();
|
||||||
|
fireEvent.change(field(form, "Timezone"), { target: { value: "Mars/Olympus_Mons" } });
|
||||||
|
writeResponse = () =>
|
||||||
|
jsonResponse({ detail: "timezone must be an IANA time zone name such as 'America/New_York'" }, 422);
|
||||||
|
save(form);
|
||||||
|
const message = await screen.findByTestId("preferences-message");
|
||||||
|
expect(message.textContent).toBe(
|
||||||
|
"The preferences could not be saved: timezone must be an IANA time zone name such as 'America/New_York'",
|
||||||
|
);
|
||||||
|
expect(field(form, "Timezone").value).toBe("Mars/Olympus_Mons");
|
||||||
|
expect(field(form, "Timezone").disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("a failed load shows the reason and Try again refetches", async () => {
|
it("a failed load shows the reason and Try again refetches", async () => {
|
||||||
let fail = true;
|
let fail = true;
|
||||||
fetchMock.mockImplementation(async (url: string, init?: RequestInit) => {
|
fetchMock.mockImplementation(async (url: string, init?: RequestInit) => {
|
||||||
@@ -512,3 +555,114 @@ describe("SettingsScreen — preferences", () => {
|
|||||||
await screen.findByRole("form", { name: "Preferences" });
|
await screen.findByRole("form", { name: "Preferences" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("SettingsScreen — timezone auto-set", () => {
|
||||||
|
let zoneSpy: ReturnType<typeof vi.spyOn> | null;
|
||||||
|
|
||||||
|
function mockBrowserZone(timeZone: string | undefined) {
|
||||||
|
const real = Intl.DateTimeFormat.prototype.resolvedOptions;
|
||||||
|
zoneSpy = vi
|
||||||
|
.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions")
|
||||||
|
.mockImplementation(function (this: Intl.DateTimeFormat) {
|
||||||
|
return { ...real.call(this), timeZone } as Intl.ResolvedDateTimeFormatOptions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionStorage.clear();
|
||||||
|
storeTokens({ access_token: "at-123", expires_in: 600 });
|
||||||
|
children = [child(1, "Declan", 1)];
|
||||||
|
settings = { ...DEFAULT_SETTINGS, timezone: "" };
|
||||||
|
writeResponse = null;
|
||||||
|
zoneSpy = null;
|
||||||
|
fetchMock = vi.fn(async (url: string, init?: RequestInit) => routeFetch(url, init));
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
zoneSpy?.mockRestore();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function renderForm() {
|
||||||
|
render(<SettingsScreen onClose={vi.fn()} />);
|
||||||
|
await screen.findByTestId("settings-child-1");
|
||||||
|
return screen.findByRole("form", { name: "Preferences" });
|
||||||
|
}
|
||||||
|
|
||||||
|
it("an unset timezone is set once, on load, to the browser's zone and shown", async () => {
|
||||||
|
mockBrowserZone("America/New_York");
|
||||||
|
const form = await renderForm();
|
||||||
|
expect(field(form, "Timezone").value).toBe("America/New_York");
|
||||||
|
expect(writes()).toEqual([
|
||||||
|
{ method: "PATCH", path: SETTINGS_PATH, body: { timezone: "America/New_York" } },
|
||||||
|
]);
|
||||||
|
expect(settings.timezone).toBe("America/New_York");
|
||||||
|
|
||||||
|
// A later save of another field does not re-send the timezone.
|
||||||
|
fireEvent.change(field(form, "Planning horizon (days)"), { target: { value: "21" } });
|
||||||
|
fireEvent.click(within(form).getByRole("button", { name: "Save preferences" }));
|
||||||
|
await screen.findByText("Preferences saved.");
|
||||||
|
expect(writes()).toHaveLength(2);
|
||||||
|
expect(writes()[1].body).toEqual({ horizon_days: 21 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("StrictMode's doubled effects still auto-set exactly once", async () => {
|
||||||
|
mockBrowserZone("America/New_York");
|
||||||
|
render(
|
||||||
|
<StrictMode>
|
||||||
|
<SettingsScreen onClose={vi.fn()} />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
const form = await screen.findByRole("form", { name: "Preferences" });
|
||||||
|
expect(field(form, "Timezone").value).toBe("America/New_York");
|
||||||
|
expect(writes()).toEqual([
|
||||||
|
{ method: "PATCH", path: SETTINGS_PATH, body: { timezone: "America/New_York" } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("an admin clearing the timezone is not re-set by the browser zone", async () => {
|
||||||
|
mockBrowserZone("America/New_York");
|
||||||
|
const form = await renderForm();
|
||||||
|
fireEvent.change(field(form, "Timezone"), { target: { value: "" } });
|
||||||
|
fireEvent.click(within(form).getByRole("button", { name: "Save preferences" }));
|
||||||
|
await screen.findByText("Preferences saved.");
|
||||||
|
expect(writes().map((w) => w.body)).toEqual([{ timezone: "America/New_York" }, { timezone: "" }]);
|
||||||
|
const reloaded = screen.getByRole("form", { name: "Preferences" });
|
||||||
|
expect(field(reloaded, "Timezone").value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a set timezone is shown and never overwritten by the browser zone", async () => {
|
||||||
|
mockBrowserZone("America/New_York");
|
||||||
|
settings = { ...DEFAULT_SETTINGS, timezone: "Europe/Dublin" };
|
||||||
|
const form = await renderForm();
|
||||||
|
expect(field(form, "Timezone").value).toBe("Europe/Dublin");
|
||||||
|
expect(writes()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["nothing", undefined],
|
||||||
|
["an empty zone", ""],
|
||||||
|
["an unknown zone", "Etc/Unknown"],
|
||||||
|
["a non-IANA zone", "not a zone"],
|
||||||
|
])("detection returning %s leaves the field empty and sends nothing", async (_case, zone) => {
|
||||||
|
mockBrowserZone(zone);
|
||||||
|
const form = await renderForm();
|
||||||
|
expect(field(form, "Timezone").value).toBe("");
|
||||||
|
expect(screen.queryByTestId("preferences-message")).toBeNull();
|
||||||
|
expect(writes()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a rejected auto-set shows why and leaves the field empty for manual entry", async () => {
|
||||||
|
mockBrowserZone("America/New_York");
|
||||||
|
writeResponse = () => jsonResponse({ detail: "timezone is not known" }, 422);
|
||||||
|
const form = await renderForm();
|
||||||
|
expect(field(form, "Timezone").value).toBe("");
|
||||||
|
expect(field(form, "Timezone").disabled).toBe(false);
|
||||||
|
expect(screen.getByTestId("preferences-message").textContent).toBe(
|
||||||
|
"The timezone could not be set to America/New_York: timezone is not known",
|
||||||
|
);
|
||||||
|
expect(writes()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
* household's child profiles. Every write refetches the list.
|
* household's child profiles. Every write refetches the list.
|
||||||
*
|
*
|
||||||
* Preferences: the household settings (planning horizon, day rollover,
|
* Preferences: the household settings (planning horizon, day rollover,
|
||||||
* notifications). Save patches only the changed fields.
|
* timezone, notifications). Save patches only the changed fields. An unset
|
||||||
|
* timezone is set once, on load, to the browser's own zone.
|
||||||
*
|
*
|
||||||
* One write at a time across both sections.
|
* One write at a time across both sections.
|
||||||
*/
|
*/
|
||||||
@@ -252,6 +253,56 @@ const NOTIFICATIONS: { label: string; time: TimeField; toggle: ToggleField }[] =
|
|||||||
|
|
||||||
const HH_MM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
const HH_MM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||||
|
|
||||||
|
/** "UTC" or an IANA-looking "Area/Location" (the API checks it resolves). */
|
||||||
|
const IANA_ZONE = /^(UTC|[A-Za-z]+(\/[A-Za-z0-9_+-]+)+)$/;
|
||||||
|
|
||||||
|
/** The Timezone field's suggestions; any other IANA zone can be typed. */
|
||||||
|
export const COMMON_TIMEZONES = [
|
||||||
|
"UTC",
|
||||||
|
"America/New_York",
|
||||||
|
"America/Chicago",
|
||||||
|
"America/Denver",
|
||||||
|
"America/Phoenix",
|
||||||
|
"America/Los_Angeles",
|
||||||
|
"America/Anchorage",
|
||||||
|
"Pacific/Honolulu",
|
||||||
|
"America/Halifax",
|
||||||
|
"America/St_Johns",
|
||||||
|
"America/Toronto",
|
||||||
|
"America/Vancouver",
|
||||||
|
"America/Mexico_City",
|
||||||
|
"America/Sao_Paulo",
|
||||||
|
"Europe/London",
|
||||||
|
"Europe/Dublin",
|
||||||
|
"Europe/Paris",
|
||||||
|
"Europe/Berlin",
|
||||||
|
"Europe/Madrid",
|
||||||
|
"Europe/Rome",
|
||||||
|
"Europe/Athens",
|
||||||
|
"Africa/Johannesburg",
|
||||||
|
"Asia/Dubai",
|
||||||
|
"Asia/Kolkata",
|
||||||
|
"Asia/Singapore",
|
||||||
|
"Asia/Shanghai",
|
||||||
|
"Asia/Tokyo",
|
||||||
|
"Australia/Perth",
|
||||||
|
"Australia/Sydney",
|
||||||
|
"Pacific/Auckland",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The browser's own IANA zone — the location-derived default for an unset
|
||||||
|
* household timezone — or "" when it is missing or not a usable zone name.
|
||||||
|
*/
|
||||||
|
export function detectTimezone(): string {
|
||||||
|
try {
|
||||||
|
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
return typeof zone === "string" && zone !== "Etc/Unknown" && IANA_ZONE.test(zone) ? zone : "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** The form's working copy: the number is text so it can be mid-edit. */
|
/** The form's working copy: the number is text so it can be mid-edit. */
|
||||||
type PreferencesDraft = Omit<HouseholdSettings, "horizon_days"> & { horizon_days: string };
|
type PreferencesDraft = Omit<HouseholdSettings, "horizon_days"> & { horizon_days: string };
|
||||||
|
|
||||||
@@ -275,11 +326,16 @@ function buildChanges(draft: PreferencesDraft, loaded: HouseholdSettings): Setti
|
|||||||
for (const [key, label] of times) {
|
for (const [key, label] of times) {
|
||||||
if (!HH_MM.test(draft[key])) return `${label} must be a time like 07:30.`;
|
if (!HH_MM.test(draft[key])) return `${label} must be a time like 07:30.`;
|
||||||
}
|
}
|
||||||
|
const timezone = draft.timezone.trim();
|
||||||
|
if (timezone && !IANA_ZONE.test(timezone)) {
|
||||||
|
return "Timezone must be an IANA zone like America/New_York, or UTC.";
|
||||||
|
}
|
||||||
|
|
||||||
const next: HouseholdSettings = {
|
const next: HouseholdSettings = {
|
||||||
...draft,
|
...draft,
|
||||||
horizon_days: horizonDays,
|
horizon_days: horizonDays,
|
||||||
notify_target: draft.notify_target.trim(),
|
notify_target: draft.notify_target.trim(),
|
||||||
|
timezone,
|
||||||
};
|
};
|
||||||
const changes: SettingsChanges = {};
|
const changes: SettingsChanges = {};
|
||||||
for (const key of Object.keys(loaded) as (keyof HouseholdSettings)[]) {
|
for (const key of Object.keys(loaded) as (keyof HouseholdSettings)[]) {
|
||||||
@@ -357,6 +413,25 @@ function PreferencesForm({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{timeInput("day_rollover_time", "Day rollover")}
|
{timeInput("day_rollover_time", "Day rollover")}
|
||||||
|
<label className="settings-field">
|
||||||
|
<span className="settings-label">Timezone</span>
|
||||||
|
<input
|
||||||
|
className="settings-input"
|
||||||
|
type="text"
|
||||||
|
list="settings-timezones"
|
||||||
|
placeholder="Host local time"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
value={draft.timezone}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(event) => update({ timezone: event.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<datalist id="settings-timezones">
|
||||||
|
{COMMON_TIMEZONES.map((zone) => (
|
||||||
|
<option key={zone} value={zone} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
<label className="settings-field">
|
<label className="settings-field">
|
||||||
<span className="settings-label">Notify target</span>
|
<span className="settings-label">Notify target</span>
|
||||||
<input
|
<input
|
||||||
@@ -495,6 +570,8 @@ export default function SettingsScreen({ onClose }: { onClose: () => void }) {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
// A ref, not state: two taps in one frame must not both see `busy === null`.
|
// A ref, not state: two taps in one frame must not both see `busy === null`.
|
||||||
const inFlight = useRef(false);
|
const inFlight = useRef(false);
|
||||||
|
// The unset-timezone auto-set fires at most once per screen.
|
||||||
|
const timezoneAutoSet = useRef(false);
|
||||||
const screenRef = useRef<HTMLDivElement>(null);
|
const screenRef = useRef<HTMLDivElement>(null);
|
||||||
const titleRef = useRef<HTMLHeadingElement>(null);
|
const titleRef = useRef<HTMLHeadingElement>(null);
|
||||||
const addRef = useRef<HTMLButtonElement>(null);
|
const addRef = useRef<HTMLButtonElement>(null);
|
||||||
@@ -539,8 +616,27 @@ export default function SettingsScreen({ onClose }: { onClose: () => void }) {
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setPreferences({ kind: "loading" });
|
setPreferences({ kind: "loading" });
|
||||||
fetchSettings()
|
fetchSettings()
|
||||||
.then((settings) => {
|
.then(async (settings) => {
|
||||||
if (!cancelled) setPreferences({ kind: "ready", settings });
|
if (cancelled) return;
|
||||||
|
// Unset: adopt the browser's zone before the form shows, so nothing
|
||||||
|
// the admin enters can be overwritten by it.
|
||||||
|
const detected = settings.timezone === "" ? detectTimezone() : "";
|
||||||
|
if (!detected || timezoneAutoSet.current) {
|
||||||
|
setPreferences({ kind: "ready", settings });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timezoneAutoSet.current = true;
|
||||||
|
try {
|
||||||
|
const saved = await updateSettings({ timezone: detected });
|
||||||
|
if (!cancelled) setPreferences({ kind: "ready", settings: saved });
|
||||||
|
} catch (error) {
|
||||||
|
if (cancelled) return;
|
||||||
|
setPreferences({ kind: "ready", settings });
|
||||||
|
setPreferencesMessage({
|
||||||
|
kind: "error",
|
||||||
|
text: errorMessage(error, `The timezone could not be set to ${detected}`),
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"issue_tracker": "https://github.com/Maniacs-Mansion/NestQuest/issues",
|
"issue_tracker": "https://github.com/Maniacs-Mansion/NestQuest/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"version": "0.9.2"
|
"version": "0.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nestquest"
|
name = "nestquest"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
description = "NestQuest Home Assistant Integration"
|
description = "NestQuest Home Assistant Integration"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
Reference in New Issue
Block a user