mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-11 00:50:40 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da6561515 | ||
|
|
070251f360 | ||
|
|
11d38fcd6a | ||
|
|
ce6da0c03d | ||
|
|
011af7c8ea | ||
|
|
94aa373387 | ||
|
|
c5bfbd7478 | ||
|
|
72fc9c7278 | ||
|
|
79cf8b0f04 | ||
|
|
4a8fca8bb8 | ||
|
|
6a18686b1c | ||
|
|
f456bd7dc6 | ||
|
|
78a2d4d6df | ||
|
|
0f8d7b4d41 | ||
|
|
cf3009f408 | ||
|
|
bdf629f836 | ||
|
|
464b58af04 | ||
|
|
29a75209ac | ||
|
|
f89b3c23c7 | ||
|
|
29b631b960 | ||
|
|
dd94a7bac1 | ||
|
|
34154195c8 | ||
|
|
0b34fcb559 |
@@ -32,12 +32,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||
uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||
uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@eada3c96a64734dd381cfbda23511034e328ddb0 # v7.6.0
|
||||
- uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@ab22029681aa532bfe7de5774a9972d67bfbd2c0 # master
|
||||
uses: home-assistant/actions/helpers/verify-version@a7c616ce81ccda50150bf1595786c71b1883fabb # master
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
@@ -76,19 +76,21 @@ jobs:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: |
|
||||
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
|
||||
# Wait for the package to become available on PyPI
|
||||
echo "Waiting for home-assistant-frontend==$version to appear on PyPI..."
|
||||
# Wait for the exact wheel to appear on the simple index (the surface
|
||||
# the wheels build's pip uses). The JSON API can report a version as
|
||||
# available before it propagates here, which fails the wheels build.
|
||||
wheel="home_assistant_frontend-${version}-py3-none-any.whl"
|
||||
echo "Waiting for $wheel to appear on the PyPI simple index..."
|
||||
for i in $(seq 1 30); do
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/home-assistant-frontend/$version/json")
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "Package is available on PyPI!"
|
||||
if curl -sf "https://pypi.org/simple/home-assistant-frontend/" | grep -qF "$wheel"; then
|
||||
echo "Package is available on the PyPI simple index!"
|
||||
break
|
||||
fi
|
||||
if [ "$i" = "30" ]; then
|
||||
echo "Timed out waiting for package to appear on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "Not available yet (HTTP $status), retrying in 30 seconds... ($i/30)"
|
||||
echo "Not available yet, retrying in 30 seconds... ($i/30)"
|
||||
sleep 30
|
||||
done
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
||||
@@ -228,6 +228,7 @@ export default [
|
||||
"entity-state",
|
||||
"ha-markdown",
|
||||
"integration-card",
|
||||
"cloud-account",
|
||||
"box-shadow",
|
||||
"util-long-press",
|
||||
"remove-delete-add-create",
|
||||
|
||||
@@ -106,6 +106,17 @@ export class DemoHaSelectBox extends LitElement {
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<label>Disabled with a selected option</label>
|
||||
<ha-select-box
|
||||
.value=${"card"}
|
||||
.options=${fullOptions}
|
||||
.disabled=${true}
|
||||
>
|
||||
</ha-select-box>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Column layout</b></p>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Cloud account
|
||||
---
|
||||
|
||||
The [Home Assistant Cloud](https://www.nabucasa.com/) account page, rendered from
|
||||
mocked cloud data. The controls at the top flip the mocked subscription, remote,
|
||||
backup, onboarding, and feature state so every UI state can be previewed here
|
||||
without a real cloud account.
|
||||
@@ -0,0 +1,568 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../src/components/ha-select";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../src/components/ha-switch";
|
||||
import type { BackupConfig } from "../../../../src/data/backup";
|
||||
import { BackupScheduleRecurrence } from "../../../../src/data/backup";
|
||||
import type {
|
||||
CloudStatusLoggedIn,
|
||||
RemoteCertificateStatus,
|
||||
SubscriptionInfo,
|
||||
} from "../../../../src/data/cloud";
|
||||
import { ONBOARDING_ITEMS } from "../../../../src/data/cloud";
|
||||
import type { Webhook } from "../../../../src/data/webhook";
|
||||
import type { ShowDialogParams } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import "../../../../src/panels/config/cloud/account/cloud-account-onboarding";
|
||||
import "../../../../src/panels/config/cloud/account/cloud-account-overview";
|
||||
import { onboardingComplete } from "../../../../src/panels/config/cloud/account/cloud-account-status";
|
||||
import { showCloudOnboardingDialog } from "../../../../src/panels/config/cloud/account/show-dialog-cloud-onboarding";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
// This demo renders the self-contained cloud account cards (overview +
|
||||
// onboarding) directly, driven by mocked data, with controls to flip the
|
||||
// subscription, remote, backup, onboarding, and feature state so every UI state
|
||||
// can be previewed. It intentionally does NOT render the <cloud-account> panel
|
||||
// wrapper, which adds a hass-subpage toolbar/back button and route links that
|
||||
// have no home in the gallery. See src/panels/config/cloud/account.
|
||||
|
||||
// The five PaymentSubscriptionState values.
|
||||
type DemoCloudAccount =
|
||||
"active" | "trialing" | "canceled" | "expired" | "unknown";
|
||||
|
||||
// "local": automatic backups run, but only to the local agent (no cloud copy).
|
||||
// "none": no automatic backups at all.
|
||||
type DemoCloudBackup = "fresh" | "stale" | "failed" | "local" | "none";
|
||||
|
||||
interface CloudDemoScenario {
|
||||
account: DemoCloudAccount;
|
||||
onboarded: boolean;
|
||||
postponed: boolean;
|
||||
remote: boolean;
|
||||
remoteStatus: RemoteCertificateStatus;
|
||||
backup: DemoCloudBackup;
|
||||
alexa: boolean;
|
||||
google: boolean;
|
||||
webrtc: boolean;
|
||||
webhooks: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SCENARIO: CloudDemoScenario = {
|
||||
account: "active",
|
||||
// Onboarding not completed and streaming (WebRTC) left off, so the onboarding
|
||||
// card shows by default with streaming as the remaining step.
|
||||
onboarded: false,
|
||||
postponed: false,
|
||||
remote: true,
|
||||
remoteStatus: "ready",
|
||||
backup: "fresh",
|
||||
alexa: true,
|
||||
google: true,
|
||||
webrtc: false,
|
||||
webhooks: true,
|
||||
};
|
||||
|
||||
const SUBSCRIPTION_OPTIONS: { value: DemoCloudAccount; label: string }[] = [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "trialing", label: "Trialing" },
|
||||
{ value: "canceled", label: "Canceled" },
|
||||
{ value: "expired", label: "Expired" },
|
||||
{ value: "unknown", label: "Unknown" },
|
||||
];
|
||||
|
||||
const REMOTE_STATUS_OPTIONS: {
|
||||
value: RemoteCertificateStatus;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ value: "ready", label: "Ready" },
|
||||
{ value: "generating", label: "Preparing" },
|
||||
{ value: "loading", label: "Loading" },
|
||||
{ value: "loaded", label: "Loaded" },
|
||||
{ value: "error", label: "Error" },
|
||||
];
|
||||
|
||||
const BACKUP_OPTIONS: { value: DemoCloudBackup; label: string }[] = [
|
||||
{ value: "fresh", label: "Recent" },
|
||||
{ value: "stale", label: "Old" },
|
||||
{ value: "failed", label: "Failed" },
|
||||
{ value: "local", label: "Local only" },
|
||||
{ value: "none", label: "None" },
|
||||
];
|
||||
|
||||
const TOGGLES: [keyof CloudDemoScenario, string][] = [
|
||||
["onboarded", "Onboarded"],
|
||||
["postponed", "Onboarding postponed"],
|
||||
["remote", "Remote access"],
|
||||
["alexa", "Alexa linked"],
|
||||
["google", "Google linked"],
|
||||
["webrtc", "Cameras (WebRTC)"],
|
||||
["webhooks", "Has webhooks"],
|
||||
];
|
||||
|
||||
const CLOUD_AGENT = "cloud.cloud";
|
||||
|
||||
const emptyFilter = () => ({
|
||||
include_domains: [],
|
||||
include_entities: [],
|
||||
exclude_domains: [],
|
||||
exclude_entities: [],
|
||||
});
|
||||
|
||||
const demoWebhooks: Webhook[] = [
|
||||
{
|
||||
webhook_id: "demo_front_door",
|
||||
domain: "automation",
|
||||
name: "Front door motion",
|
||||
local_only: false,
|
||||
},
|
||||
{
|
||||
webhook_id: "demo_companion_app",
|
||||
domain: "mobile_app",
|
||||
name: "Companion app",
|
||||
local_only: false,
|
||||
},
|
||||
];
|
||||
|
||||
const buildSubscription = (scenario: CloudDemoScenario): SubscriptionInfo => ({
|
||||
human_description: "Demo subscription, renews automatically",
|
||||
provider: "Nabu Casa, Inc.",
|
||||
plan_renewal_date: 4102444800,
|
||||
subscription: { status: scenario.account },
|
||||
});
|
||||
|
||||
const buildCloudStatus = (scenario: CloudDemoScenario): CloudStatusLoggedIn => {
|
||||
const active =
|
||||
scenario.account !== "canceled" && scenario.account !== "expired";
|
||||
const cloudhooks = scenario.webhooks
|
||||
? Object.fromEntries(
|
||||
demoWebhooks.map((webhook) => [
|
||||
webhook.webhook_id,
|
||||
{
|
||||
webhook_id: webhook.webhook_id,
|
||||
cloudhook_id: `demo-${webhook.webhook_id}`,
|
||||
cloudhook_url: `https://hooks.nabu.casa/demo-${webhook.webhook_id}`,
|
||||
managed: false,
|
||||
},
|
||||
])
|
||||
)
|
||||
: {};
|
||||
return {
|
||||
logged_in: true,
|
||||
cloud: "connected",
|
||||
cloud_last_disconnect_reason: null,
|
||||
email: "[email protected]",
|
||||
google_registered: scenario.google,
|
||||
google_entities: emptyFilter(),
|
||||
google_domains: ["light", "switch", "climate", "cover"],
|
||||
alexa_registered: scenario.alexa,
|
||||
alexa_entities: emptyFilter(),
|
||||
remote_domain: "demo-instance.ui.nabu.casa",
|
||||
remote_connected: scenario.remote,
|
||||
remote_certificate: {
|
||||
common_name: "demo-instance.ui.nabu.casa",
|
||||
expire_date: "2099-01-01T00:00:00+00:00",
|
||||
fingerprint: "demodemodemodemodemodemodemodemodemodemodemodemodemo",
|
||||
alternative_names: ["demo-instance.ui.nabu.casa"],
|
||||
},
|
||||
remote_certificate_status: scenario.remoteStatus,
|
||||
http_use_ssl: false,
|
||||
active_subscription: active,
|
||||
onboarding_postponed: scenario.postponed,
|
||||
onboarding_completed: scenario.onboarded,
|
||||
prefs: {
|
||||
google_enabled: scenario.google,
|
||||
alexa_enabled: scenario.alexa,
|
||||
remote_enabled: scenario.remote,
|
||||
remote_allow_remote_enable: true,
|
||||
strict_connection: "disabled",
|
||||
google_secure_devices_pin: undefined,
|
||||
cloudhooks,
|
||||
alexa_report_state: true,
|
||||
google_report_state: true,
|
||||
tts_default_voice: ["en-US", "JennyNeural"],
|
||||
cloud_ice_servers_enabled: scenario.webrtc,
|
||||
onboarded_items: scenario.onboarded ? [...ONBOARDING_ITEMS] : [],
|
||||
onboarding_postponed_until: scenario.postponed
|
||||
? new Date(Date.now() + 24 * 3600 * 1000).toISOString()
|
||||
: null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const buildBackupConfig = (scenario: CloudDemoScenario): BackupConfig => {
|
||||
const now = Date.now();
|
||||
const recent = new Date(now - 12 * 3600 * 1000).toISOString();
|
||||
const old = new Date(now - 5 * 86400000).toISOString();
|
||||
const future = new Date(now + 86400000).toISOString();
|
||||
const overdue = new Date(now - 6 * 3600 * 1000).toISOString();
|
||||
|
||||
const cloudEnabled =
|
||||
scenario.backup === "fresh" ||
|
||||
scenario.backup === "stale" ||
|
||||
scenario.backup === "failed";
|
||||
|
||||
let configured = true;
|
||||
let lastCompleted: string | null = null;
|
||||
let lastAttempted: string | null = null;
|
||||
let next: string | null = null;
|
||||
switch (scenario.backup) {
|
||||
case "fresh":
|
||||
case "local":
|
||||
lastCompleted = recent;
|
||||
lastAttempted = recent;
|
||||
next = future;
|
||||
break;
|
||||
case "stale":
|
||||
lastCompleted = old;
|
||||
lastAttempted = old;
|
||||
next = overdue;
|
||||
break;
|
||||
case "failed":
|
||||
lastCompleted = old;
|
||||
lastAttempted = recent;
|
||||
next = future;
|
||||
break;
|
||||
case "none":
|
||||
configured = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
automatic_backups_configured: configured,
|
||||
last_attempted_automatic_backup: lastAttempted,
|
||||
last_completed_automatic_backup: lastCompleted,
|
||||
next_automatic_backup: next,
|
||||
next_automatic_backup_additional: false,
|
||||
create_backup: {
|
||||
agent_ids: cloudEnabled
|
||||
? ["backup.local", CLOUD_AGENT]
|
||||
: ["backup.local"],
|
||||
include_addons: [],
|
||||
include_all_addons: true,
|
||||
include_database: true,
|
||||
include_folders: [],
|
||||
name: null,
|
||||
password: null,
|
||||
},
|
||||
retention: { copies: 3, days: null },
|
||||
schedule: {
|
||||
recurrence: BackupScheduleRecurrence.DAILY,
|
||||
time: null,
|
||||
days: [],
|
||||
},
|
||||
agents: {
|
||||
"backup.local": { protected: true, retention: null },
|
||||
"cloud.cloud": { protected: true, retention: null },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("demo-misc-cloud-account")
|
||||
export class DemoMiscCloudAccount
|
||||
extends LitElement
|
||||
implements ProvideHassElement
|
||||
{
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
@state() private _scenario: CloudDemoScenario = { ...DEFAULT_SCENARIO };
|
||||
|
||||
@state() private _cloudStatus!: CloudStatusLoggedIn;
|
||||
|
||||
@state() private _subscription!: SubscriptionInfo;
|
||||
|
||||
@state() private _backupConfig!: BackupConfig;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
|
||||
hass.updateHass({
|
||||
config: {
|
||||
...hass.config,
|
||||
components: [
|
||||
...(hass.config?.components ?? []),
|
||||
"cloud",
|
||||
"backup",
|
||||
"webhook",
|
||||
],
|
||||
},
|
||||
});
|
||||
this._registerMocks(hass);
|
||||
this._applyScenario();
|
||||
}
|
||||
|
||||
public provideHass(el) {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._showDialog);
|
||||
this.addEventListener("cloud-open-onboarding", this._openOnboarding);
|
||||
this.addEventListener("ha-refresh-cloud-status", this._refreshFromMocks);
|
||||
// The overview and onboarding dialog contain real <a href="/config/..">
|
||||
// links to panel routes that do not exist in the gallery. Keep them inert.
|
||||
this.addEventListener("click", this._neutralizeNavigation);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("show-dialog", this._showDialog);
|
||||
this.removeEventListener("cloud-open-onboarding", this._openOnboarding);
|
||||
this.removeEventListener("ha-refresh-cloud-status", this._refreshFromMocks);
|
||||
this.removeEventListener("click", this._neutralizeNavigation);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
const showOnboarding =
|
||||
this._cloudStatus.active_subscription &&
|
||||
!this._cloudStatus.onboarding_completed &&
|
||||
!this._cloudStatus.onboarding_postponed &&
|
||||
!onboardingComplete(this._cloudStatus, this._backupConfig);
|
||||
|
||||
return html`
|
||||
<div class="options">
|
||||
<div class="selects">
|
||||
${this._select("Subscription", "account", SUBSCRIPTION_OPTIONS)}
|
||||
${this._select("Remote status", "remoteStatus", REMOTE_STATUS_OPTIONS)}
|
||||
${this._select("Backups", "backup", BACKUP_OPTIONS)}
|
||||
</div>
|
||||
<div class="switches">
|
||||
${TOGGLES.map(([field, label]) => this._toggle(label, field))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview">
|
||||
${
|
||||
showOnboarding
|
||||
? html`
|
||||
<cloud-account-onboarding
|
||||
.hass=${this.hass}
|
||||
.cloudStatus=${this._cloudStatus}
|
||||
.backupConfig=${this._backupConfig}
|
||||
></cloud-account-onboarding>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<cloud-account-overview
|
||||
.hass=${this.hass}
|
||||
.cloudStatus=${this._cloudStatus}
|
||||
.subscription=${this._subscription}
|
||||
.backupConfig=${this._backupConfig}
|
||||
.webhooks=${demoWebhooks}
|
||||
></cloud-account-overview>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _select(
|
||||
label: string,
|
||||
field: keyof CloudDemoScenario,
|
||||
options: { value: string; label: string }[]
|
||||
) {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${label}
|
||||
.value=${String(this._scenario[field])}
|
||||
.options=${options}
|
||||
data-field=${field}
|
||||
@selected=${this._selectChanged}
|
||||
></ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggle(label: string, field: keyof CloudDemoScenario) {
|
||||
return html`
|
||||
<ha-formfield .label=${label}>
|
||||
<ha-switch
|
||||
.checked=${this._scenario[field] as boolean}
|
||||
data-field=${field}
|
||||
@change=${this._switchChanged}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectChanged(ev: HaSelectSelectEvent) {
|
||||
const field = (ev.currentTarget as HTMLElement).dataset
|
||||
.field as keyof CloudDemoScenario;
|
||||
this._setField(field, ev.detail.value as string);
|
||||
}
|
||||
|
||||
private _switchChanged(ev: Event) {
|
||||
const target = ev.target as HaSwitch;
|
||||
this._setField(
|
||||
target.dataset.field as keyof CloudDemoScenario,
|
||||
target.checked
|
||||
);
|
||||
}
|
||||
|
||||
private _setField(field: keyof CloudDemoScenario, value: string | boolean) {
|
||||
if (this._scenario[field] === value) {
|
||||
return;
|
||||
}
|
||||
this._scenario = { ...this._scenario, [field]: value };
|
||||
this._applyScenario();
|
||||
}
|
||||
|
||||
private _applyScenario() {
|
||||
this._cloudStatus = buildCloudStatus(this._scenario);
|
||||
this._subscription = buildSubscription(this._scenario);
|
||||
this._backupConfig = buildBackupConfig(this._scenario);
|
||||
}
|
||||
|
||||
private _openOnboarding = () => {
|
||||
showCloudOnboardingDialog(this, {
|
||||
cloudStatus: this._cloudStatus,
|
||||
backupConfig: this._backupConfig,
|
||||
onChanged: () => this._refreshFromMocks(),
|
||||
});
|
||||
};
|
||||
|
||||
private _showDialog = (ev: HASSDomEvent<ShowDialogParams<unknown>>) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } =
|
||||
ev.detail;
|
||||
showDialog(
|
||||
this,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
parentElement,
|
||||
addHistory
|
||||
);
|
||||
};
|
||||
|
||||
private _refreshFromMocks = () => {
|
||||
this._cloudStatus = {
|
||||
...this._cloudStatus,
|
||||
prefs: { ...this._cloudStatus.prefs },
|
||||
};
|
||||
this._backupConfig = { ...this._backupConfig };
|
||||
const cloudBackup =
|
||||
this._backupConfig.create_backup.agent_ids.includes(CLOUD_AGENT);
|
||||
this._scenario = {
|
||||
...this._scenario,
|
||||
onboarded: this._cloudStatus.onboarding_completed,
|
||||
postponed: this._cloudStatus.onboarding_postponed,
|
||||
remote: this._cloudStatus.prefs.remote_enabled,
|
||||
webrtc: this._cloudStatus.prefs.cloud_ice_servers_enabled,
|
||||
backup: !this._backupConfig.automatic_backups_configured
|
||||
? "none"
|
||||
: cloudBackup
|
||||
? this._scenario.backup === "fresh" ||
|
||||
this._scenario.backup === "stale" ||
|
||||
this._scenario.backup === "failed"
|
||||
? this._scenario.backup
|
||||
: "fresh"
|
||||
: "local",
|
||||
};
|
||||
};
|
||||
|
||||
private _neutralizeNavigation = (ev: MouseEvent) => {
|
||||
const anchor = ev
|
||||
.composedPath()
|
||||
.find((el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement);
|
||||
if (anchor?.getAttribute("href")?.startsWith("/")) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private _registerMocks(hass: MockHomeAssistant) {
|
||||
hass.mockWS("cloud/status", () => ({
|
||||
...this._cloudStatus,
|
||||
prefs: { ...this._cloudStatus.prefs },
|
||||
}));
|
||||
|
||||
hass.mockWS("cloud/update_prefs", (msg) => {
|
||||
const { type, ...prefs } = msg;
|
||||
this._cloudStatus.prefs = { ...this._cloudStatus.prefs, ...prefs };
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
hass.mockWS("cloud/onboarding/postpone", () => {
|
||||
this._cloudStatus.onboarding_postponed = true;
|
||||
this._cloudStatus.prefs.onboarding_postponed_until = new Date(
|
||||
Date.now() + 24 * 3600 * 1000
|
||||
).toISOString();
|
||||
return { ...this._cloudStatus, prefs: { ...this._cloudStatus.prefs } };
|
||||
});
|
||||
|
||||
hass.mockWS("cloud/remote/connect", () => {
|
||||
this._cloudStatus.remote_connected = true;
|
||||
this._cloudStatus.prefs.remote_enabled = true;
|
||||
return null;
|
||||
});
|
||||
hass.mockWS("cloud/remote/disconnect", () => {
|
||||
this._cloudStatus.remote_connected = false;
|
||||
this._cloudStatus.prefs.remote_enabled = false;
|
||||
return null;
|
||||
});
|
||||
|
||||
hass.mockWS("backup/config/info", () => ({
|
||||
config: { ...this._backupConfig },
|
||||
}));
|
||||
hass.mockWS("backup/config/update", (msg) => {
|
||||
const { type, ...update } = msg;
|
||||
if (update.create_backup) {
|
||||
this._backupConfig.create_backup = {
|
||||
...this._backupConfig.create_backup,
|
||||
...update.create_backup,
|
||||
};
|
||||
}
|
||||
if (update.automatic_backups_configured !== undefined) {
|
||||
this._backupConfig.automatic_backups_configured =
|
||||
update.automatic_backups_configured;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.options {
|
||||
max-width: 600px;
|
||||
margin: 16px auto 0;
|
||||
padding: 0 16px 16px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.selects {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.selects ha-select {
|
||||
min-width: 160px;
|
||||
flex: 1;
|
||||
}
|
||||
.switches {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 16px;
|
||||
}
|
||||
.preview {
|
||||
padding: 24px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-6, 24px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-misc-cloud-account": DemoMiscCloudAccount;
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -49,7 +49,7 @@
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/search": "6.7.1",
|
||||
"@codemirror/state": "6.7.1",
|
||||
"@codemirror/view": "6.43.7",
|
||||
"@codemirror/view": "6.43.8",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.6.0",
|
||||
@@ -93,7 +93,7 @@
|
||||
"cally": "0.9.2",
|
||||
"color-name": "2.1.1",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.49.0",
|
||||
"core-js": "3.50.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.4.0",
|
||||
@@ -103,7 +103,7 @@
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.6.16",
|
||||
"hls.js": "1.6.17",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.13",
|
||||
@@ -114,7 +114,7 @@
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "18.0.7",
|
||||
"marked": "18.0.9",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -152,8 +152,8 @@
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.62.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.1",
|
||||
"@rspack/core": "2.1.7",
|
||||
"@rspack/dev-server": "2.1.0",
|
||||
"@rspack/core": "2.1.8",
|
||||
"@rspack/dev-server": "2.2.0",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -210,7 +210,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.65.0",
|
||||
"typescript-eslint": "8.66.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.10",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -23,24 +23,24 @@ export const isNavigationClick = (e: MouseEvent, preventDefault = true) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let href = anchor.href;
|
||||
if (!href || href.indexOf("mailto:") !== -1) {
|
||||
let url: URL;
|
||||
try {
|
||||
// anchor.href is always absolute; an empty or unparseable value throws.
|
||||
url = new URL(anchor.href);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const location = window.location;
|
||||
const origin = location.origin || location.protocol + "//" + location.host;
|
||||
if (!href.startsWith(origin)) {
|
||||
return undefined;
|
||||
}
|
||||
href = href.slice(origin.length);
|
||||
|
||||
if (href === "#") {
|
||||
// Only intercept same-origin links. A different scheme, host, or port is a
|
||||
// different origin (e.g. another port like ":8123") and must trigger a full
|
||||
// browser navigation instead of an in-app route change. Non-http(s) schemes
|
||||
// such as mailto: resolve to a null origin and are excluded here too.
|
||||
if (url.origin !== window.location.origin) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return href;
|
||||
return url.pathname + url.search + url.hash;
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
case "timer":
|
||||
return compareState === "active";
|
||||
case "camera":
|
||||
return compareState === "streaming";
|
||||
return ["streaming", "recording"].includes(compareState);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -18,6 +18,37 @@ interface MinMaxFrame {
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
// Frame sizes that divide the clock evenly. Frames are placed on absolute time
|
||||
// rather than relative to the window, so charts that follow "now" keep picking
|
||||
// the same points every redraw instead of redrawing with a different shape.
|
||||
const FRAME_SIZES = [
|
||||
[1, 2, 3, 5, 10, 20, 30, 50, 100, 200, 300, 500],
|
||||
[1, 2, 3, 5, 10, 15, 20, 30].map((n) => n * SECOND),
|
||||
[1, 2, 3, 5, 10, 15, 20, 30].map((n) => n * MINUTE),
|
||||
[1, 2, 3, 4, 6, 8, 12].map((n) => n * HOUR),
|
||||
[DAY],
|
||||
].flat();
|
||||
|
||||
// Always rounds down, so no chart ends up with fewer frames than it asked for.
|
||||
function snapFrameSize(step: number): number {
|
||||
if (step >= DAY) {
|
||||
return Math.floor(step / DAY) * DAY;
|
||||
}
|
||||
let snapped = FRAME_SIZES[0];
|
||||
for (const size of FRAME_SIZES) {
|
||||
if (size > step) {
|
||||
break;
|
||||
}
|
||||
snapped = size;
|
||||
}
|
||||
return snapped;
|
||||
}
|
||||
|
||||
export function downSampleLineData<
|
||||
T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number],
|
||||
>(
|
||||
@@ -35,11 +66,13 @@ export function downSampleLineData<
|
||||
}
|
||||
const min = minX ?? getPointData(data[0]!)[0];
|
||||
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
|
||||
const step = Math.ceil((max - min) / Math.floor(maxDetails));
|
||||
if (!Number.isFinite(step) || step <= 0) {
|
||||
const rawStep = Math.ceil((max - min) / Math.floor(maxDetails));
|
||||
if (!Number.isFinite(rawStep) || rawStep <= 0) {
|
||||
// a degenerate frame size would put every point in a single frame
|
||||
return data;
|
||||
}
|
||||
// snapped after the guard above, which relies on the unsnapped value
|
||||
const step = snapFrameSize(rawStep);
|
||||
|
||||
if (useMean) {
|
||||
// Group points into frames, accumulating sums in insertion order.
|
||||
@@ -52,7 +85,7 @@ export function downSampleLineData<
|
||||
const y = Number(pointData[1]);
|
||||
if (isNaN(x) || isNaN(y)) continue;
|
||||
|
||||
const frameIndex = Math.floor((x - min) / step);
|
||||
const frameIndex = Math.floor(x / step);
|
||||
const frame = frames.get(frameIndex);
|
||||
if (!frame) {
|
||||
frames.set(frameIndex, {
|
||||
@@ -90,7 +123,7 @@ export function downSampleLineData<
|
||||
const y = Number(pointData[1]);
|
||||
if (isNaN(x) || isNaN(y)) continue;
|
||||
|
||||
const frameIndex = Math.floor((x - min) / step);
|
||||
const frameIndex = Math.floor(x / step);
|
||||
const frame = frames.get(frameIndex);
|
||||
if (!frame) {
|
||||
frames.set(frameIndex, {
|
||||
|
||||
@@ -56,6 +56,7 @@ export class HaSelectBox extends LitElement {
|
||||
class="list"
|
||||
style=${styleMap({ "--columns": columns })}
|
||||
.value=${this.value}
|
||||
?disabled=${this.disabled}
|
||||
@change=${this._radioChanged}
|
||||
>
|
||||
${this.options.map((option) => this._renderOption(option))}
|
||||
@@ -100,7 +101,7 @@ export class HaSelectBox extends LitElement {
|
||||
)}
|
||||
aria-labelledby=${`label-${option.value}`}
|
||||
.value=${option.value}
|
||||
.disabled=${disabled}
|
||||
.disabled=${option.disabled || false}
|
||||
></ha-radio-option>
|
||||
<div class="text">
|
||||
<span id=${`label-${option.value}`} class="label"
|
||||
|
||||
@@ -53,7 +53,6 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
value: behavior,
|
||||
label: this._localizeOption(behavior, "label"),
|
||||
description: this._localizeOption(behavior, "description"),
|
||||
disabled: this.disabled,
|
||||
...(isTrigger && {
|
||||
image: {
|
||||
src: `/static/images/form/automation_behavior_trigger_${behavior}.svg`,
|
||||
@@ -66,6 +65,7 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
<ha-select-box
|
||||
.options=${options}
|
||||
.value=${this.value ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
max_columns="1"
|
||||
?stacked_image=${isTrigger}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
||||
@@ -104,6 +104,7 @@ export class HaSelectSelector extends LitElement {
|
||||
<ha-select-box
|
||||
.options=${options}
|
||||
.value=${this.value as string | undefined}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._selectChanged}
|
||||
.maxColumns=${this.selector.select?.box_max_columns}
|
||||
></ha-select-box>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type {
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { DOMAINS_WITH_DYNAMIC_PICTURE } from "../common/const";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stateActive } from "../common/entity/state_active";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { HomeAssistant, ServiceCallResponse } from "../types";
|
||||
|
||||
@@ -66,6 +70,75 @@ export type SceneMetaData = Record<
|
||||
{ entity_only?: boolean | undefined }
|
||||
>;
|
||||
|
||||
// Hand-edited scenes.yaml is parsed as YAML 1.1 by the backend, so unquoted
|
||||
// on/off arrive here as booleans. The backend applies boolean states as
|
||||
// on/off; mirror that so the badge reflects what activating the scene will
|
||||
// actually do. Everything else the backend rejects (numbers, null, arrays,
|
||||
// objects) yields undefined.
|
||||
const normalizeSceneEntityState = (sceneState: unknown): string | undefined => {
|
||||
if (typeof sceneState === "boolean") {
|
||||
return sceneState ? "on" : "off";
|
||||
}
|
||||
if (typeof sceneState === "string") {
|
||||
return sceneState;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Builds a state object from the scene's stored target state, so the scene
|
||||
// editor can render the icon the entity will have once the scene is applied
|
||||
// rather than its current live icon. The parameter is typed unknown because
|
||||
// the scene config API returns raw YAML: booleans, numbers, nulls, and
|
||||
// malformed shapes occur in hand-edited files beyond what the SceneEntities
|
||||
// union declares. Entries that hold no usable target state (an entity left
|
||||
// without a value in the YAML editor parses as null, a dict may lack a state
|
||||
// key) yield undefined so the caller renders no badge instead of guessing.
|
||||
//
|
||||
// The result is a partial HassEntity meant for badge rendering only - it has
|
||||
// no last_changed, last_updated, or context.
|
||||
export const sceneEntityStateObj = (
|
||||
entityId: string,
|
||||
sceneEntity: unknown
|
||||
): HassEntity | undefined => {
|
||||
if (
|
||||
typeof sceneEntity !== "object" ||
|
||||
sceneEntity === null ||
|
||||
Array.isArray(sceneEntity)
|
||||
) {
|
||||
const state = normalizeSceneEntityState(sceneEntity);
|
||||
return state === undefined
|
||||
? undefined
|
||||
: ({ entity_id: entityId, state, attributes: {} } as HassEntity);
|
||||
}
|
||||
const { state: sceneState, ...attributes } = sceneEntity as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const state = normalizeSceneEntityState(sceneState);
|
||||
if (state === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// Media-derived entity pictures are snapshotted with an access token that
|
||||
// is stale by the time review mode renders, which would leave the badge
|
||||
// showing a broken image instead of an icon. Stable pictures on other
|
||||
// domains are kept - the same policy createHistoricState applies for the
|
||||
// logbook.
|
||||
if (DOMAINS_WITH_DYNAMIC_PICTURE.has(computeDomain(entityId))) {
|
||||
delete attributes.entity_picture;
|
||||
delete attributes.entity_picture_local;
|
||||
}
|
||||
const stateObj = { entity_id: entityId, state, attributes } as HassEntity;
|
||||
// A live entity never carries color attributes while off, and state-badge
|
||||
// applies rgb_color and brightness without checking activity; drop them for
|
||||
// inactive targets so a scene that turns a light off does not render an
|
||||
// active-looking colored icon.
|
||||
if (!stateActive(stateObj)) {
|
||||
delete attributes.rgb_color;
|
||||
delete attributes.brightness;
|
||||
}
|
||||
return stateObj;
|
||||
};
|
||||
|
||||
export const activateScene = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
|
||||
@@ -102,13 +102,12 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
) {
|
||||
this.checkDataBaseMigration();
|
||||
}
|
||||
// Wait for `hass.user` to populate so the admin guard can run; it arrives
|
||||
// asynchronously after `hass.config`.
|
||||
if (
|
||||
changedProps.has("hass") &&
|
||||
this.hass?.user &&
|
||||
oldHass?.user !== this.hass.user
|
||||
) {
|
||||
// Wait for `hass.user` to first populate so the admin guard can run; it
|
||||
// arrives asynchronously after `hass.config`. `hass.user` also gets a fresh
|
||||
// reference at runtime (reconnect, profile refresh via subscribeUser), so
|
||||
// only trigger on the initial population (null -> user). Reconnect re-checks
|
||||
// come from connection-mixin, the launch-screen swap re-check from update().
|
||||
if (changedProps.has("hass") && this.hass?.user && !oldHass?.user) {
|
||||
this.checkHttpPendingConfig();
|
||||
}
|
||||
if (
|
||||
@@ -125,12 +124,12 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
if (
|
||||
this.hass?.states &&
|
||||
this.hass.config &&
|
||||
this.hass.services &&
|
||||
this._databaseMigration === false
|
||||
) {
|
||||
const removingLaunchScreen =
|
||||
!!this.hass?.states &&
|
||||
!!this.hass.config &&
|
||||
!!this.hass.services &&
|
||||
this._databaseMigration === false;
|
||||
if (removingLaunchScreen) {
|
||||
this.render = this.renderHass;
|
||||
this.update = super.update;
|
||||
// partial-panel-resolver removes the launch screen after the first panel
|
||||
@@ -138,6 +137,13 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
// screen covers the frontend until frontend/loaded is sent.
|
||||
}
|
||||
super.update(changedProps);
|
||||
if (removingLaunchScreen) {
|
||||
// Surface the HTTP pending config dialog only after super.update() has
|
||||
// committed the render swap above, which clears the launch screen from
|
||||
// the shadow root. Appending the dialog before that render would let it
|
||||
// tear the freshly-added dialog straight back out of the DOM.
|
||||
this.checkHttpPendingConfig();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
@@ -256,6 +262,13 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
if (__DEMO__ || this._httpPendingDialogOpen) {
|
||||
return;
|
||||
}
|
||||
// Only show once the main UI is rendered. During startup the root swaps
|
||||
// the launch screen for the app, which clears its shadow root and would
|
||||
// tear the freshly-appended dialog straight back out of the DOM (closing
|
||||
// it). When called too early we skip; the swap in update() re-runs this.
|
||||
if (this.render !== this.renderHass) {
|
||||
return;
|
||||
}
|
||||
if (!this.hass?.user?.is_admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,8 +128,12 @@ class DialogAutomationSave
|
||||
`;
|
||||
}
|
||||
|
||||
private get _isDiscardDialog(): boolean {
|
||||
return this._params?.onDiscard !== undefined;
|
||||
}
|
||||
|
||||
protected _renderDiscard() {
|
||||
if (!this._params?.onDiscard) {
|
||||
if (!this._isDiscardDialog) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
@@ -324,10 +328,14 @@ class DialogAutomationSave
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!!this._params.config.alias && !this.isDirtyState}
|
||||
.disabled=${
|
||||
!!this._params.config.alias &&
|
||||
!this._isDiscardDialog &&
|
||||
!this.isDirtyState
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._params.config.alias && !this._params.onDiscard
|
||||
this._params.config.alias && !this._isDiscardDialog
|
||||
? "ui.panel.config.automation.editor.rename"
|
||||
: "ui.common.save"
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { mdiInformationOutline } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/input/ha-input";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import type { InputBoolean } from "../../../../data/input_boolean";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -22,6 +30,8 @@ class HaInputBooleanForm extends LitElement {
|
||||
|
||||
@state() private _icon!: string;
|
||||
|
||||
@state() private _initial?: boolean;
|
||||
|
||||
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
|
||||
|
||||
set item(item: InputBoolean) {
|
||||
@@ -29,9 +39,11 @@ class HaInputBooleanForm extends LitElement {
|
||||
if (item) {
|
||||
this._name = item.name || "";
|
||||
this._icon = item.icon || "";
|
||||
this._initial = item.initial;
|
||||
} else {
|
||||
this._name = "";
|
||||
this._icon = "";
|
||||
this._initial = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +82,79 @@ class HaInputBooleanForm extends LitElement {
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
></ha-icon-picker>
|
||||
<ha-expansion-panel
|
||||
header=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.generic.more_options"
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<ha-radio-group
|
||||
.value=${
|
||||
this._initial === undefined
|
||||
? "restore"
|
||||
: this._initial
|
||||
? "on"
|
||||
: "off"
|
||||
}
|
||||
.disabled=${this.disabled}
|
||||
name="initial"
|
||||
@change=${this._initialChanged}
|
||||
>
|
||||
<span slot="label">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.initial"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
id="initial-note"
|
||||
tabindex="0"
|
||||
class="note-icon"
|
||||
.path=${mdiInformationOutline}
|
||||
@click=${stopPropagation}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<ha-radio-option value="restore">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.restore"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="on">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.turn_on"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="off">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.turn_off"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-tooltip for="initial-note" placement="top">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.initial_helper"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</ha-expansion-panel>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _initialChanged(ev: Event) {
|
||||
const option = String((ev.currentTarget as HaRadioGroup).value);
|
||||
const initial = option === "restore" ? undefined : option === "on";
|
||||
if (initial === this._initial) {
|
||||
return;
|
||||
}
|
||||
const newValue = { ...this._item } as InputBoolean;
|
||||
if (initial === undefined) {
|
||||
delete newValue.initial;
|
||||
} else {
|
||||
newValue.initial = initial;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
if (!this.new && !this._item) {
|
||||
return;
|
||||
@@ -108,6 +189,18 @@ class HaInputBooleanForm extends LitElement {
|
||||
ha-input {
|
||||
margin: var(--ha-space-2) 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
ha-expansion-panel ha-radio-group {
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
.note-icon {
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-icon-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ class HaConfigHttpForm extends LitElement {
|
||||
"ui.panel.config.network.http.restart_address.text"
|
||||
)}
|
||||
</p>
|
||||
<a href=${url} rel="noreferrer noopener">${url}</a>
|
||||
<a href=${url} target="_blank" rel="noreferrer noopener">${url}</a>
|
||||
<p class="dialog-note">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.http.restart_address.note"
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
mdiPlaylistEdit,
|
||||
mdiTag,
|
||||
} from "@mdi/js";
|
||||
import type { HassEvent } from "home-assistant-js-websocket";
|
||||
import type { HassEntity, HassEvent } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
getSceneEditorInitData,
|
||||
saveScene,
|
||||
SCENE_IGNORED_DOMAINS,
|
||||
sceneEntityStateObj,
|
||||
showSceneEditor,
|
||||
} from "../../../data/scene";
|
||||
import {
|
||||
@@ -447,13 +448,15 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
const integrationName = platform
|
||||
? domainToName(this.hass.localize, platform)
|
||||
: undefined;
|
||||
const badgeStateObj = this._badgeStateObj(
|
||||
entityId,
|
||||
entityStateObj
|
||||
);
|
||||
return html`
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
?twoline=${!!secondary}
|
||||
.graphic=${
|
||||
this._mode === "live" ? "icon" : undefined
|
||||
}
|
||||
graphic="icon"
|
||||
.entityId=${entityId}
|
||||
@click=${
|
||||
this._mode === "live"
|
||||
@@ -463,10 +466,10 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
.noninteractive=${this._mode === "review"}
|
||||
>
|
||||
${
|
||||
this._mode === "live"
|
||||
badgeStateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.stateObj=${entityStateObj}
|
||||
.stateObj=${badgeStateObj}
|
||||
slot="graphic"
|
||||
></state-badge>
|
||||
`
|
||||
@@ -552,14 +555,16 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
this.hass.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
const badgeStateObj = this._badgeStateObj(
|
||||
entityId,
|
||||
entityStateObj
|
||||
);
|
||||
return html`
|
||||
<ha-list-item
|
||||
class="entity"
|
||||
hasMeta
|
||||
?twoline=${!!secondary}
|
||||
.graphic=${
|
||||
this._mode === "live" ? "icon" : undefined
|
||||
}
|
||||
graphic="icon"
|
||||
.entityId=${entityId}
|
||||
@click=${
|
||||
this._mode === "live"
|
||||
@@ -569,11 +574,13 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
.noninteractive=${this._mode === "review"}
|
||||
>
|
||||
${
|
||||
this._mode === "live"
|
||||
? html` <state-badge
|
||||
.stateObj=${entityStateObj}
|
||||
slot="graphic"
|
||||
></state-badge>`
|
||||
badgeStateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.stateObj=${badgeStateObj}
|
||||
slot="graphic"
|
||||
></state-badge>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${primary}
|
||||
@@ -1188,6 +1195,33 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
return { ...stateObj.attributes, state: stateObj.state };
|
||||
}
|
||||
|
||||
// Memoized per config so re-renders reuse the same object references and
|
||||
// the state badges skip work when nothing changed.
|
||||
private _sceneStateObjs = memoizeOne((config?: SceneConfig) => {
|
||||
const objs: Record<string, HassEntity | undefined> = {};
|
||||
for (const entityId of Object.keys(config?.entities ?? {})) {
|
||||
objs[entityId] = sceneEntityStateObj(
|
||||
entityId,
|
||||
config!.entities[entityId]
|
||||
);
|
||||
}
|
||||
return objs;
|
||||
});
|
||||
|
||||
// Picks the state the row's icon should reflect: the live state in live
|
||||
// mode, the scene's stored target in review mode. Undefined in review mode
|
||||
// when the scene holds no usable target for the entity - the row then
|
||||
// renders no badge rather than a live state that could be mistaken for a
|
||||
// target.
|
||||
private _badgeStateObj(
|
||||
entityId: string,
|
||||
entityStateObj: HassEntity
|
||||
): HassEntity | undefined {
|
||||
return this._mode === "live"
|
||||
? entityStateObj
|
||||
: this._sceneStateObjs(this._config)[entityId];
|
||||
}
|
||||
|
||||
private _generateConfigFromLive() {
|
||||
this._config = {
|
||||
...this._config!,
|
||||
|
||||
@@ -2142,6 +2142,13 @@
|
||||
"icon": "Icon",
|
||||
"more_options": "[%key:ui::dialogs::restart::more_options%]"
|
||||
},
|
||||
"input_boolean": {
|
||||
"initial": "Each time Home Assistant starts",
|
||||
"initial_helper": "Changing this does not affect the toggle right now.",
|
||||
"restore": "Restore previous state",
|
||||
"turn_on": "Turn on",
|
||||
"turn_off": "Turn off"
|
||||
},
|
||||
"input_datetime": {
|
||||
"date": "Date",
|
||||
"time": "Time",
|
||||
@@ -3854,7 +3861,7 @@
|
||||
},
|
||||
"disable_view_transition": {
|
||||
"title": "Disable view transitions",
|
||||
"description": "Disable animated view transitions to prevent browser crash when using browser dev tools."
|
||||
"description": "Disable animated view transitions to prevent browser crash when using browser developer tools."
|
||||
},
|
||||
"entity_diagnostic": {
|
||||
"title": "Entity diagnostic",
|
||||
@@ -3899,7 +3906,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"title": "Actions",
|
||||
"description": "The actions dev tool allows you to perform any action available in Home Assistant.",
|
||||
"description": "The actions tool allows you to perform any action available in Home Assistant.",
|
||||
"call_service": "Perform action",
|
||||
"response": "Response",
|
||||
"column_parameter": "Parameter",
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { isNavigationClick } from "../../../src/common/dom/is-navigation-click";
|
||||
|
||||
const createAnchor = (
|
||||
href: string,
|
||||
attributes: Record<string, string> = {}
|
||||
): HTMLAnchorElement => {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = href;
|
||||
for (const [name, value] of Object.entries(attributes)) {
|
||||
anchor.setAttribute(name, value);
|
||||
}
|
||||
return anchor;
|
||||
};
|
||||
|
||||
const clickEvent = (
|
||||
anchor: HTMLAnchorElement,
|
||||
overrides: Partial<MouseEvent> = {}
|
||||
): MouseEvent =>
|
||||
({
|
||||
defaultPrevented: false,
|
||||
button: 0,
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
shiftKey: false,
|
||||
composedPath: () => [anchor],
|
||||
preventDefault: vi.fn(),
|
||||
...overrides,
|
||||
}) as unknown as MouseEvent;
|
||||
|
||||
describe("isNavigationClick", () => {
|
||||
it("returns the path for same-origin links", () => {
|
||||
expect(
|
||||
isNavigationClick(clickEvent(createAnchor(`${location.origin}/config`)))
|
||||
).toEqual("/config");
|
||||
expect(
|
||||
isNavigationClick(
|
||||
clickEvent(createAnchor(`${location.origin}/energy?historyBack=1`))
|
||||
)
|
||||
).toEqual("/energy?historyBack=1");
|
||||
expect(
|
||||
isNavigationClick(
|
||||
clickEvent(createAnchor(`${location.origin}/config/areas#section`))
|
||||
)
|
||||
).toEqual("/config/areas#section");
|
||||
});
|
||||
|
||||
it("does not intercept a link on a different port", () => {
|
||||
// The current origin has no port, so a ported URL shares its string prefix
|
||||
// but is a different origin — it must not be treated as internal.
|
||||
expect(
|
||||
isNavigationClick(clickEvent(createAnchor(`${location.origin}:8123/`)))
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not intercept a host that merely starts with the origin", () => {
|
||||
expect(
|
||||
isNavigationClick(
|
||||
clickEvent(createAnchor(`${location.origin}.example.com/`))
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not intercept other origins", () => {
|
||||
expect(
|
||||
isNavigationClick(clickEvent(createAnchor("https://example.com/")))
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("calls preventDefault for intercepted navigations", () => {
|
||||
const event = clickEvent(createAnchor(`${location.origin}/config`));
|
||||
isNavigationClick(event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not preventDefault when disabled", () => {
|
||||
const event = clickEvent(createAnchor(`${location.origin}/config`));
|
||||
expect(isNavigationClick(event, false)).toEqual("/config");
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores clicks with a target, download, or rel=external", () => {
|
||||
expect(
|
||||
isNavigationClick(
|
||||
clickEvent(
|
||||
createAnchor(`${location.origin}/config`, { target: "_blank" })
|
||||
)
|
||||
)
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
isNavigationClick(
|
||||
clickEvent(createAnchor(`${location.origin}/config`, { download: "" }))
|
||||
)
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
isNavigationClick(
|
||||
clickEvent(
|
||||
createAnchor(`${location.origin}/config`, { rel: "external" })
|
||||
)
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores modified clicks and non-left buttons", () => {
|
||||
const anchor = createAnchor(`${location.origin}/config`);
|
||||
expect(
|
||||
isNavigationClick(clickEvent(anchor, { button: 1 }))
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
isNavigationClick(clickEvent(anchor, { metaKey: true }))
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
isNavigationClick(clickEvent(anchor, { ctrlKey: true }))
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
isNavigationClick(clickEvent(anchor, { shiftKey: true }))
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
isNavigationClick(clickEvent(anchor, { defaultPrevented: true }))
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores mailto links and clicks without an anchor", () => {
|
||||
expect(
|
||||
isNavigationClick(clickEvent(createAnchor("mailto:[email protected]")))
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
isNavigationClick(
|
||||
clickEvent(undefined as any, {
|
||||
composedPath: () => [document.createElement("div")],
|
||||
})
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,6 +96,91 @@ describe("downSampleLineData", () => {
|
||||
expect(downSampleLineData(points, 40, minX, maxX)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// A window that follows "now" must keep sampling the same points, otherwise
|
||||
// the line redraws with a different shape every few seconds. See #53542.
|
||||
const HALF_DAY_OF_5S_SAMPLES = 8640;
|
||||
const SIX_HOURS = 6 * 3_600_000;
|
||||
// The frames straddling either end of a window are only partly filled, so
|
||||
// they legitimately differ between two windows. Ignore a frame's width.
|
||||
const EDGE_MARGIN = 60_000;
|
||||
|
||||
it.each([
|
||||
["min/max mode", false],
|
||||
["mean mode", true],
|
||||
])("%s samples the same points when the window slides", (_name, useMean) => {
|
||||
const points = generatePoints(12, HALF_DAY_OF_5S_SAMPLES, 5_000);
|
||||
const start = FIXED_EPOCH_MS + 3_600_000;
|
||||
const sample = (offset: number) =>
|
||||
downSampleLineData(
|
||||
points.filter(
|
||||
([x]) => x >= start + offset && x <= start + offset + SIX_HOURS
|
||||
),
|
||||
600,
|
||||
start + offset,
|
||||
start + offset + SIX_HOURS,
|
||||
useMean
|
||||
) as [number, number][];
|
||||
|
||||
const reference = sample(0);
|
||||
expect(reference.length).toBeLessThan(HALF_DAY_OF_5S_SAMPLES / 2);
|
||||
|
||||
for (const offset of [10_000, 30_000, 60_000]) {
|
||||
const inOverlap = (result: [number, number][]) =>
|
||||
result.filter(
|
||||
([x]) =>
|
||||
x > start + offset + EDGE_MARGIN &&
|
||||
x < start + SIX_HOURS - EDGE_MARGIN
|
||||
);
|
||||
expect(inOverlap(sample(offset))).toEqual(inOverlap(reference));
|
||||
}
|
||||
});
|
||||
|
||||
it("min/max mode samples the same points when the window grows", () => {
|
||||
const points = generatePoints(13, HALF_DAY_OF_5S_SAMPLES, 5_000);
|
||||
const start = FIXED_EPOCH_MS;
|
||||
const sample = (growth: number) =>
|
||||
downSampleLineData(
|
||||
points.filter(([x]) => x <= start + SIX_HOURS + growth),
|
||||
600,
|
||||
start,
|
||||
start + SIX_HOURS + growth
|
||||
) as [number, number][];
|
||||
|
||||
const drawn = (result: [number, number][]) =>
|
||||
result.filter(([x]) => x < start + SIX_HOURS - EDGE_MARGIN);
|
||||
const reference = sample(0);
|
||||
expect(drawn(reference).length).toBeGreaterThan(100);
|
||||
|
||||
for (const growth of [10_000, 30_000, 60_000]) {
|
||||
expect(drawn(sample(growth))).toEqual(drawn(reference));
|
||||
}
|
||||
});
|
||||
|
||||
it("aligns frames to absolute time, not to the start of the window", () => {
|
||||
// An hour of 5s samples over a 120 frame budget resolves to 30s frames
|
||||
const FRAME = 30_000;
|
||||
const start = FIXED_EPOCH_MS;
|
||||
const points = generatePoints(14, 720, 5_000);
|
||||
const result = downSampleLineData(
|
||||
points,
|
||||
120,
|
||||
start,
|
||||
start + 3_600_000
|
||||
) as [number, number][];
|
||||
|
||||
const framed = new Map<number, number[]>();
|
||||
points.forEach(([x, y]) => {
|
||||
const frame = Math.floor(x / FRAME);
|
||||
framed.set(frame, [...(framed.get(frame) ?? []), y]);
|
||||
});
|
||||
expect(result.length).toBeLessThan(points.length);
|
||||
result.forEach(([x, y]) => {
|
||||
const values = framed.get(Math.floor(x / FRAME));
|
||||
expect(values).toBeDefined();
|
||||
expect([Math.min(...values!), Math.max(...values!)]).toContain(y);
|
||||
});
|
||||
});
|
||||
|
||||
it("small scale digest is stable", () => {
|
||||
expect(
|
||||
digestResult(downSampleLineData(generatePoints(8, SCALES.small), 500))
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { sceneEntityStateObj } from "../../src/data/scene";
|
||||
|
||||
describe("sceneEntityStateObj", () => {
|
||||
it("builds a state object from string shorthand", () => {
|
||||
expect(sceneEntityStateObj("light.kitchen", "on")).toEqual({
|
||||
entity_id: "light.kitchen",
|
||||
state: "on",
|
||||
attributes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("builds a state object from the dict form", () => {
|
||||
expect(
|
||||
sceneEntityStateObj("light.kitchen", {
|
||||
state: "on",
|
||||
brightness: 180,
|
||||
rgb_color: [255, 64, 112],
|
||||
})
|
||||
).toEqual({
|
||||
entity_id: "light.kitchen",
|
||||
state: "on",
|
||||
attributes: { brightness: 180, rgb_color: [255, 64, 112] },
|
||||
});
|
||||
});
|
||||
|
||||
it("strips media-derived entity pictures but keeps other attributes", () => {
|
||||
const sceneEntity = {
|
||||
state: "playing",
|
||||
entity_picture: "/api/media_player_proxy/x?token=stale",
|
||||
entity_picture_local: "/local/x.jpg",
|
||||
friendly_name: "Player",
|
||||
};
|
||||
expect(sceneEntityStateObj("media_player.x", sceneEntity)).toEqual({
|
||||
entity_id: "media_player.x",
|
||||
state: "playing",
|
||||
attributes: { friendly_name: "Player" },
|
||||
});
|
||||
// The input scene config must not be mutated.
|
||||
expect(sceneEntity.entity_picture).toBe(
|
||||
"/api/media_player_proxy/x?token=stale"
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a stable entity picture on other domains", () => {
|
||||
expect(
|
||||
sceneEntityStateObj("vacuum.robot", {
|
||||
state: "cleaning",
|
||||
entity_picture: "/local/robot.png",
|
||||
})?.attributes.entity_picture
|
||||
).toBe("/local/robot.png");
|
||||
});
|
||||
|
||||
it("returns undefined for null and undefined values", () => {
|
||||
// An entity left without a value in the YAML editor parses as null.
|
||||
expect(sceneEntityStateObj("light.kitchen", null)).toBeUndefined();
|
||||
expect(sceneEntityStateObj("light.kitchen", undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for an array value", () => {
|
||||
expect(sceneEntityStateObj("light.kitchen", [255, 100])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for a numeric value", () => {
|
||||
// The backend only accepts string and boolean states, so a number never
|
||||
// becomes a target the scene can apply.
|
||||
expect(sceneEntityStateObj("input_number.x", 23)).toBeUndefined();
|
||||
expect(
|
||||
sceneEntityStateObj("input_number.x", { state: 23 })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when the dict holds no usable state", () => {
|
||||
expect(
|
||||
sceneEntityStateObj("light.kitchen", { brightness: 100 })
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
sceneEntityStateObj("light.kitchen", { state: null })
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
sceneEntityStateObj("light.kitchen", { state: { brightness: 100 } })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes boolean shorthand to on/off like the backend does", () => {
|
||||
// YAML 1.1 parses unquoted on/off in scenes.yaml as booleans.
|
||||
expect(sceneEntityStateObj("light.kitchen", true)?.state).toBe("on");
|
||||
expect(sceneEntityStateObj("light.kitchen", false)?.state).toBe("off");
|
||||
});
|
||||
|
||||
it("normalizes a boolean state in the dict form", () => {
|
||||
expect(sceneEntityStateObj("light.kitchen", { state: true })).toEqual({
|
||||
entity_id: "light.kitchen",
|
||||
state: "on",
|
||||
attributes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("drops color attributes for an inactive target", () => {
|
||||
// A live entity never carries these while off, and state-badge applies
|
||||
// them without checking activity.
|
||||
expect(
|
||||
sceneEntityStateObj("light.kitchen", {
|
||||
state: "off",
|
||||
brightness: 180,
|
||||
rgb_color: [255, 0, 0],
|
||||
friendly_name: "Kitchen",
|
||||
})
|
||||
).toEqual({
|
||||
entity_id: "light.kitchen",
|
||||
state: "off",
|
||||
attributes: { friendly_name: "Kitchen" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2439,15 +2439,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/view@npm:6.43.7, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
|
||||
version: 6.43.7
|
||||
resolution: "@codemirror/view@npm:6.43.7"
|
||||
"@codemirror/view@npm:6.43.8, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
|
||||
version: 6.43.8
|
||||
resolution: "@codemirror/view@npm:6.43.8"
|
||||
dependencies:
|
||||
"@codemirror/state": "npm:^6.7.0"
|
||||
crelt: "npm:^1.0.6"
|
||||
style-mod: "npm:^4.1.0"
|
||||
w3c-keyname: "npm:^2.2.4"
|
||||
checksum: 10/dbc7c4e9162099dae15be1107e831c955c778fa224e24f4fd02d6eb9e75bc6c6d636e3e9fce3dc5852576c392c5793f4f6c5838f932a9c9e77f32a4ca66f62bf
|
||||
checksum: 10/88cd5543b09c4b945862ff128aeea48e259a582e1106fbc22370ddf773e629810b43baaf3abdf743150098e07c601ca481186cdf46b537f0312452fa2e7e8f51
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2561,13 +2561,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/core@npm:1.11.2":
|
||||
version: 1.11.2
|
||||
resolution: "@emnapi/core@npm:1.11.2"
|
||||
"@emnapi/core@npm:1.11.3":
|
||||
version: 1.11.3
|
||||
resolution: "@emnapi/core@npm:1.11.3"
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads": "npm:1.2.2"
|
||||
"@emnapi/wasi-threads": "npm:1.2.3"
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/b3e9693f79ca1d8bb90cb8a950150c7738f54eca0ae1849d3d5103a87ce4e388c2669fb253cc123d3449ad2ae12583435455dbdc0270b1e9efb0cfd502c952b4
|
||||
checksum: 10/6ed871d5bbd0f6e25a09fe91583e886e4b90ae9344fa9292c78f0891b0b2a7d84899ea65a7d576450b9ac50440c698dad1907907e3d686ec9460ed876a9363a2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2589,12 +2589,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/runtime@npm:1.11.2":
|
||||
version: 1.11.2
|
||||
resolution: "@emnapi/runtime@npm:1.11.2"
|
||||
"@emnapi/runtime@npm:1.11.3":
|
||||
version: 1.11.3
|
||||
resolution: "@emnapi/runtime@npm:1.11.3"
|
||||
dependencies:
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/280a219d3bf302615dabaaa248223b0e5fe9c49bacf0987dd958ca37ab425b62cf05287053cb188e711681418aaa5e447eaed6b62a0848c0a1303b2707864600
|
||||
checksum: 10/ec834cc0fe248e06dd84f6dee10162133f8a692636189e99aa992303c791d4be1679536ee91aeee6661c4478609768cee9c085acd858caa92784c67acaab44a5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2616,6 +2616,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/wasi-threads@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@emnapi/wasi-threads@npm:1.2.3"
|
||||
dependencies:
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/d9fef5c321a6df1988e813e0b621ed1e01d274c23d3028ff9511af97f8935adc921ae60a35167ccf4e0414fecc040b66b9b13458e7c00cfaad622457e1b03529
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1":
|
||||
version: 4.9.1
|
||||
resolution: "@eslint-community/eslint-utils@npm:4.9.1"
|
||||
@@ -4756,110 +4765,110 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-arm64@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.1.7"
|
||||
"@rspack/binding-darwin-arm64@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.1.8"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-x64@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.1.7"
|
||||
"@rspack/binding-darwin-x64@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.1.8"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.7"
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.8"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.7"
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.8"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-riscv64-gnu@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.7"
|
||||
"@rspack/binding-linux-riscv64-gnu@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.8"
|
||||
conditions: os=linux & cpu=riscv64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-riscv64-musl@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.7"
|
||||
"@rspack/binding-linux-riscv64-musl@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.8"
|
||||
conditions: os=linux & cpu=riscv64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.7"
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.8"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-musl@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.7"
|
||||
"@rspack/binding-linux-x64-musl@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.8"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-wasm32-wasi@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.7"
|
||||
"@rspack/binding-wasm32-wasi@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.8"
|
||||
dependencies:
|
||||
"@emnapi/core": "npm:1.11.2"
|
||||
"@emnapi/runtime": "npm:1.11.2"
|
||||
"@emnapi/core": "npm:1.11.3"
|
||||
"@emnapi/runtime": "npm:1.11.3"
|
||||
"@napi-rs/wasm-runtime": "npm:1.1.6"
|
||||
conditions: cpu=wasm32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.7"
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.8"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.7"
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.8"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.7"
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.8"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/binding@npm:2.1.7"
|
||||
"@rspack/binding@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/binding@npm:2.1.8"
|
||||
dependencies:
|
||||
"@rspack/binding-darwin-arm64": "npm:2.1.7"
|
||||
"@rspack/binding-darwin-x64": "npm:2.1.7"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.1.7"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.1.7"
|
||||
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.7"
|
||||
"@rspack/binding-linux-riscv64-musl": "npm:2.1.7"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.1.7"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.1.7"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.1.7"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.1.7"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.1.7"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.1.7"
|
||||
"@rspack/binding-darwin-arm64": "npm:2.1.8"
|
||||
"@rspack/binding-darwin-x64": "npm:2.1.8"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.1.8"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.1.8"
|
||||
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.8"
|
||||
"@rspack/binding-linux-riscv64-musl": "npm:2.1.8"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.1.8"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.1.8"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.1.8"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.1.8"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.1.8"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.1.8"
|
||||
dependenciesMeta:
|
||||
"@rspack/binding-darwin-arm64":
|
||||
optional: true
|
||||
@@ -4885,15 +4894,15 @@ __metadata:
|
||||
optional: true
|
||||
"@rspack/binding-win32-x64-msvc":
|
||||
optional: true
|
||||
checksum: 10/1400d8553d2122850fc43a45b69828bcdcbccf7715381a627802e160da6f93e90ca41d0280a69d175894bc29a428a374c80157b874883363b998090dc0aff33c
|
||||
checksum: 10/74c73ac543059fa62365557ca949ef7a84e1cebc42fe51a343126c875d90f557b35a52b33bacd9498d9231e8fdb8ab13907988e1fd39131ffbd56becb4260785
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/core@npm:2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@rspack/core@npm:2.1.7"
|
||||
"@rspack/core@npm:2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@rspack/core@npm:2.1.8"
|
||||
dependencies:
|
||||
"@rspack/binding": "npm:2.1.7"
|
||||
"@rspack/binding": "npm:2.1.8"
|
||||
peerDependencies:
|
||||
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
|
||||
"@swc/helpers": ^0.5.23
|
||||
@@ -4902,7 +4911,7 @@ __metadata:
|
||||
optional: true
|
||||
"@swc/helpers":
|
||||
optional: true
|
||||
checksum: 10/84526df889fa2813cfa7dfbed71b713c47c2010b1f55da26db698a87ea11f3f433a931ec29616ff300cb63d0ea27cbe7d2b40df64abf45f9b1a484b59c94ca1a
|
||||
checksum: 10/c818257225a24feb22af81eb3ed7d02d4b74d6e6cb70689c8cbcd34e3facfcfe833dfe4078e0a35277e926f431cae9fd7f2101c3944b396a72baa7cf3ce99d19
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4918,9 +4927,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/dev-server@npm:2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "@rspack/dev-server@npm:2.1.0"
|
||||
"@rspack/dev-server@npm:2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "@rspack/dev-server@npm:2.2.0"
|
||||
dependencies:
|
||||
"@rspack/dev-middleware": "npm:^2.0.3"
|
||||
peerDependencies:
|
||||
@@ -4929,7 +4938,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
selfsigned:
|
||||
optional: true
|
||||
checksum: 10/402d4f96c60beaba354081a36b053c3cf7c1dda7fddab791031dc4206b9873b98437895d5a6a68247e07cb8a5922a1770143c560772dfdaa00ba90a5396251d8
|
||||
checksum: 10/edfc57223fad64c43c96be95b0b6650194ac2e02d929bef649d524faa9c618233afe8f970ac2e21c77e1b92f6c65dac20f06817e73a9bd661f43c73057ae6b6b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5762,105 +5771,105 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.65.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.66.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
"@typescript-eslint/scope-manager": "npm:8.65.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.65.0"
|
||||
"@typescript-eslint/utils": "npm:8.65.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.65.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.66.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.66.0"
|
||||
"@typescript-eslint/utils": "npm:8.66.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.66.0"
|
||||
ignore: "npm:^7.0.5"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.65.0
|
||||
"@typescript-eslint/parser": ^8.66.0
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/20b1e5fd0c01d450c345750582e4911affef8ba934b2b78953ff593102d2a01f21c5f6469e13239fdd6a0e30152d6122906e7623f53aa8c937c6faae9407be47
|
||||
checksum: 10/bb144ff0c27592b6a53d964fe1e2145dd0737f5fe50fe7dcd88217ca5ac4e470d7965d97745a63de16252ac2880d00732a472b9cec14d7d6929ebda97e7bcbb1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.65.0"
|
||||
"@typescript-eslint/parser@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.66.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.65.0"
|
||||
"@typescript-eslint/types": "npm:8.65.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.65.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.65.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.66.0"
|
||||
"@typescript-eslint/types": "npm:8.66.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.66.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.66.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/55f68666953c02c8adae35a46076848da6456181b1849a28ec836a5866ca37b902f475fb4c32ba7aa3a6a7e5d66828df8859edec297da09181fb937aeea30f8e
|
||||
checksum: 10/a0451abe17d2eeff6ff2e1bf637d5ea19b400378d98f4511672e0f30dc5a7971d03352e32764927176c3632f245d5f85e8b28a0a3e66d020effcf0c61624e9b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.65.0"
|
||||
"@typescript-eslint/project-service@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.66.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.65.0"
|
||||
"@typescript-eslint/types": "npm:^8.65.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.66.0"
|
||||
"@typescript-eslint/types": "npm:^8.66.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/915662449a66d90f03661a805f7c62a5efaa8f7887671272e08b684b41edab51a9021f47aac4d78001a0f87960e8587adc037424d56f13de883e0ce699e7ca55
|
||||
checksum: 10/e8a71f69ee7f4bd9ca1c6a0a5110361b9927d17df2f41cdf6d042203b49aabfbddece79dec7882fde8dc016b6278671a2ba3f7bf08e5ae241582627b68be3a8b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.65.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.66.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.65.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.65.0"
|
||||
checksum: 10/038e208c907aa45fe5bb7168e1dccf89c1fc6678d1715a9c04f4b332d4e79ab719b5b43a4e0c2d5a183ea863813ff59fda040b2717fe5517468453af6b99a50a
|
||||
"@typescript-eslint/types": "npm:8.66.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.66.0"
|
||||
checksum: 10/f2024cd2819dc3b967f32089ea1a0b9099acabf12860699432f0ba802d75b01ed35ea488a50576cae544813d5cbe9bd9ee5815bc4f11cefc89fe2fb24559238f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.65.0, @typescript-eslint/tsconfig-utils@npm:^8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.65.0"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.66.0, @typescript-eslint/tsconfig-utils@npm:^8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.66.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/f88253a4df1d599a1bebeeb403611538485e22c52213f2f2b9c435bde8ebce64645d6bb985c900b01ef221032835fcd4f6fea4b94d178220c18de5d93af905c4
|
||||
checksum: 10/271b28660edc9e6272ae07a91f8da2c69e8a6b009e8588f1009c485c11fe4e66e09356d50dbeabf8e6a44c95c8adeb630f1b6089bdd68aec2512d0036cef7704
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.65.0"
|
||||
"@typescript-eslint/type-utils@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.66.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.65.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.65.0"
|
||||
"@typescript-eslint/utils": "npm:8.65.0"
|
||||
"@typescript-eslint/types": "npm:8.66.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.66.0"
|
||||
"@typescript-eslint/utils": "npm:8.66.0"
|
||||
debug: "npm:^4.4.3"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/d52e0c341c9731d8f3bfd2f475bbcd5a5632434e11fc39e8e21acb706145bedb8c6c1998b8ced73e132b43179dd95f2c318effc36c9a1dd61f14546b07b25852
|
||||
checksum: 10/6d3c55a591b96cbac4ac845ad1d281bac54df12a631ca550370d266aed30760fa9d498e44b938e454941930e3decda820c6e544e0748254c5089425d120e1ed1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.65.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/types@npm:8.65.0"
|
||||
checksum: 10/a6fc10a733adbb98bbb9c312c8d99791e1a2fd3efef5c2a5b51da1c166aac3db4427c30bf32059808abe305a289f820bf610683914e897baeb18315af6a1d16c
|
||||
"@typescript-eslint/types@npm:8.66.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/types@npm:8.66.0"
|
||||
checksum: 10/de1ea79e7056c11e38e4001a899b88511793a0aa1702508af42ee179bc7303bed2eb02f177a7ae438f20eab00aee9407c7e842bec6fc5b7bb1fdcab4a7cd5c4b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.65.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.66.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.65.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.65.0"
|
||||
"@typescript-eslint/types": "npm:8.65.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.65.0"
|
||||
"@typescript-eslint/project-service": "npm:8.66.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.66.0"
|
||||
"@typescript-eslint/types": "npm:8.66.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.66.0"
|
||||
debug: "npm:^4.4.3"
|
||||
minimatch: "npm:^10.2.2"
|
||||
semver: "npm:^7.7.3"
|
||||
@@ -5868,32 +5877,32 @@ __metadata:
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/711fdb5eff67ff34437c56a1a661b16a2e1d6bcd96c9f96196c4242b8d4ddaea8e835ca8badd340c703e043d8dfe8cfca90b32eca60069a4f1d2880afdb670fb
|
||||
checksum: 10/0834efcdd4468c0dd954089daf26b40175e204402bd4075b06cd910786bb6de7a15aec4e79e40f0980f39fce5bfd9c871ec8fcc6871a8895a34a273bb187fd26
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.65.0"
|
||||
"@typescript-eslint/utils@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.66.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.9.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.65.0"
|
||||
"@typescript-eslint/types": "npm:8.65.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.65.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.66.0"
|
||||
"@typescript-eslint/types": "npm:8.66.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.66.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/c1dcd555b58aef1e066164978335e521809acac36b56bd6a6dae62cffae80f3ea5f43527506be76dfef0fe3d4c8382a24355a28867c4904b0a7729691ba45656
|
||||
checksum: 10/e7a24ee0f35d58b4392daa03fb1ea37ab7e4519949a034be89151df1b0e9fc416831f50cb69f54150272e252cba350964632ee9bfbdb7e62ac599cc731de56e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.65.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.66.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.65.0"
|
||||
"@typescript-eslint/types": "npm:8.66.0"
|
||||
eslint-visitor-keys: "npm:^5.0.0"
|
||||
checksum: 10/e7f86d21f0bf03ca7cff9fa9428aab98620564d15fb06c9f56a294c13cee324624d42f9115f3d76fbad92266220479cca334b5c66562f885da5c4d2bda3d87b3
|
||||
checksum: 10/d47e936b10ec94a96f69f77a62463f4634fe72d12d42dd302d7777f7b98689468e0b67c538a52232b723695208f1b3fdeae5745eb3e71a70a90818fdd827a0bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7845,10 +7854,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"core-js@npm:3.49.0":
|
||||
version: 3.49.0
|
||||
resolution: "core-js@npm:3.49.0"
|
||||
checksum: 10/31d018f9830b0240ae40869e380595f2d06a8800709ad63299a42a438ba0c8d5805045fa02a20a78f42761d83103b0b71eca982955f5e890fb7cf6b2fe6a9ab1
|
||||
"core-js@npm:3.50.0":
|
||||
version: 3.50.0
|
||||
resolution: "core-js@npm:3.50.0"
|
||||
checksum: 10/9690bd111d8500437e65b365ec0dbe8d8899e99e877452d142c72bfbf272bd6edbf38ec9c2da8fa88d9186bba9246309bff68cedd2a6c43a3bf0748625e0e092
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9859,10 +9868,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hls.js@npm:1.6.16":
|
||||
version: 1.6.16
|
||||
resolution: "hls.js@npm:1.6.16"
|
||||
checksum: 10/478a4c29a4246dff0f1209b2808f1e2e6651ef584df51fab23f6bbcb582e464f17552b9b76f3043dd98b8ae388961e5174a10c1f3dd55ec2ba7587c6bb3bd7f3
|
||||
"hls.js@npm:1.6.17":
|
||||
version: 1.6.17
|
||||
resolution: "hls.js@npm:1.6.17"
|
||||
checksum: 10/345d6c98f7f32f25f1924d34d4177f37aa9ae10c95fe6518c4ca10b295acbf174fbeba113696fbce673a1ee90461b530e101dd2b2f4d958af82577706d6e24c2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9886,7 +9895,7 @@ __metadata:
|
||||
"@codemirror/lint": "npm:6.9.7"
|
||||
"@codemirror/search": "npm:6.7.1"
|
||||
"@codemirror/state": "npm:6.7.1"
|
||||
"@codemirror/view": "npm:6.43.7"
|
||||
"@codemirror/view": "npm:6.43.8"
|
||||
"@date-fns/tz": "npm:1.5.0"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@eslint/js": "npm:10.0.1"
|
||||
@@ -9927,8 +9936,8 @@ __metadata:
|
||||
"@playwright/test": "npm:1.62.1"
|
||||
"@replit/codemirror-indentation-markers": "npm:6.5.3"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.6.1"
|
||||
"@rspack/core": "npm:2.1.7"
|
||||
"@rspack/dev-server": "npm:2.1.0"
|
||||
"@rspack/core": "npm:2.1.8"
|
||||
"@rspack/dev-server": "npm:2.2.0"
|
||||
"@swc/helpers": "npm:0.5.23"
|
||||
"@thomasloven/round-slider": "npm:0.6.0"
|
||||
"@tsparticles/engine": "npm:4.3.2"
|
||||
@@ -9960,7 +9969,7 @@ __metadata:
|
||||
cally: "npm:0.9.2"
|
||||
color-name: "npm:2.1.1"
|
||||
comlink: "npm:4.4.2"
|
||||
core-js: "npm:3.49.0"
|
||||
core-js: "npm:3.50.0"
|
||||
cropperjs: "npm:1.6.2"
|
||||
culori: "npm:4.0.2"
|
||||
date-fns: "npm:4.4.0"
|
||||
@@ -9987,7 +9996,7 @@ __metadata:
|
||||
gulp: "npm:5.0.1"
|
||||
gulp-json-transform: "npm:0.5.0"
|
||||
gulp-rename: "npm:2.1.0"
|
||||
hls.js: "npm:1.6.16"
|
||||
hls.js: "npm:1.6.17"
|
||||
home-assistant-js-websocket: "npm:9.6.0"
|
||||
html-minifier-terser: "npm:7.2.0"
|
||||
husky: "npm:9.1.7"
|
||||
@@ -10008,7 +10017,7 @@ __metadata:
|
||||
lodash.template: "npm:4.18.1"
|
||||
luxon: "npm:3.7.2"
|
||||
map-stream: "npm:0.0.7"
|
||||
marked: "npm:18.0.7"
|
||||
marked: "npm:18.0.9"
|
||||
memoize-one: "npm:6.0.0"
|
||||
minify-literals: "npm:2.1.0"
|
||||
node-vibrant: "npm:4.0.4"
|
||||
@@ -10031,7 +10040,7 @@ __metadata:
|
||||
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.65.0"
|
||||
typescript-eslint: "npm:8.66.0"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.10"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
@@ -11651,12 +11660,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:18.0.7":
|
||||
version: 18.0.7
|
||||
resolution: "marked@npm:18.0.7"
|
||||
"marked@npm:18.0.9":
|
||||
version: 18.0.9
|
||||
resolution: "marked@npm:18.0.9"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: 10/7ea7b8556a9e8cab2881b194815a7550c61d77639c6b8f8d9022f96790fd2b289b2ea28969cea6e01c24c3b8ec822373288ee02b7c50f178bc3ec79aa42d05f1
|
||||
checksum: 10/99d337c50acd57034734f8460f25b28e9658b15627f950092707cb443b840f0bd29fe343bf211b948e643303c34abdb5b5a12cf5245a846635750697524bb747
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14883,18 +14892,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:8.65.0":
|
||||
version: 8.65.0
|
||||
resolution: "typescript-eslint@npm:8.65.0"
|
||||
"typescript-eslint@npm:8.66.0":
|
||||
version: 8.66.0
|
||||
resolution: "typescript-eslint@npm:8.66.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.65.0"
|
||||
"@typescript-eslint/parser": "npm:8.65.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.65.0"
|
||||
"@typescript-eslint/utils": "npm:8.65.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.66.0"
|
||||
"@typescript-eslint/parser": "npm:8.66.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.66.0"
|
||||
"@typescript-eslint/utils": "npm:8.66.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/5b2242f59005afdd57190849be7df2e86ce33b007fa0bf605f700ad0e38ea14fc14c48deafdf4a039614c1f012b71c61a19edb27f76d52fdc924cde476a100d1
|
||||
checksum: 10/e76da9c684e4fe012b49a5120dd57553df0788cf1cffcbd5dc72c98de33e549f306495e710292f111ee058366b1e5d8d603c2bb14385b12bb35edc7bbf13ad55
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user