mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-12 02:39:26 +00:00
Compare commits
28 Commits
section-di
...
toggle_gro
Author | SHA1 | Date | |
---|---|---|---|
![]() |
134681b4c9 | ||
![]() |
082f1ca55e | ||
![]() |
3b7d2869e5 | ||
![]() |
bcda5cd0cf | ||
![]() |
eeb64a25ff | ||
![]() |
9134132ba9 | ||
![]() |
341e63e878 | ||
![]() |
5ed2d2fd2f | ||
![]() |
c6f92d1375 | ||
![]() |
e8201f7848 | ||
![]() |
6d7df18e82 | ||
![]() |
1471cfea66 | ||
![]() |
9e4835107d | ||
![]() |
1ded254e5a | ||
![]() |
fc104a7992 | ||
![]() |
3269fd3c5b | ||
![]() |
17e63343c7 | ||
![]() |
e7e062a222 | ||
![]() |
5233086efb | ||
![]() |
8d95f0d95d | ||
![]() |
5cf8b39703 | ||
![]() |
15dabe372c | ||
![]() |
aab52a8bb2 | ||
![]() |
dc7ba0dac6 | ||
![]() |
2ab4608884 | ||
![]() |
de7f5c1bb7 | ||
![]() |
aa52825b40 | ||
![]() |
2809a306e6 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -11,7 +11,7 @@ body:
|
||||
|
||||
**Please do not report issues for custom cards.**
|
||||
|
||||
[fr]: https://github.com/home-assistant/frontend/discussions
|
||||
[fr]: https://github.com/orgs/home-assistant/discussions
|
||||
[releases]: https://github.com/home-assistant/home-assistant/releases
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Request a feature for the UI / Dashboards
|
||||
url: https://github.com/home-assistant/frontend/discussions/category_choices
|
||||
url: https://github.com/orgs/home-assistant/discussions
|
||||
about: Request a new feature for the Home Assistant frontend.
|
||||
- name: Report a bug that is NOT related to the UI / Dashboards
|
||||
url: https://github.com/home-assistant/core/issues
|
||||
|
53
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Task
|
||||
description: For staff only - Create a task
|
||||
type: Task
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## ⚠️ RESTRICTED ACCESS
|
||||
|
||||
**This form is restricted to Open Home Foundation staff and authorized contributors only.**
|
||||
|
||||
If you are a community member wanting to contribute, please:
|
||||
- For bug reports: Use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)
|
||||
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
|
||||
|
||||
---
|
||||
|
||||
### For authorized contributors
|
||||
|
||||
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
Provide a clear and detailed description of the task that needs to be accomplished.
|
||||
|
||||
Be specific about what needs to be done, why it's important, and any constraints or requirements.
|
||||
placeholder: |
|
||||
Describe the task, including:
|
||||
- What needs to be done
|
||||
- Why this task is needed
|
||||
- Expected outcome
|
||||
- Any constraints or requirements
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional_context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Any additional information, links, research, or context that would be helpful.
|
||||
|
||||
Include links to related issues, research, prototypes, roadmap opportunities etc.
|
||||
placeholder: |
|
||||
- Roadmap opportunity: [link]
|
||||
- Epic: [link]
|
||||
- Feature request: [link]
|
||||
- Technical design documents: [link]
|
||||
- Prototype/mockup: [link]
|
||||
- Dependencies: [links]
|
||||
validations:
|
||||
required: false
|
58
.github/workflows/restrict-task-creation.yml
vendored
Normal file
58
.github/workflows/restrict-task-creation.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Restrict task creation
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-authorization:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.issue_type == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
|
||||
// Check if user is an organization member
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: 'home-assistant',
|
||||
username: issueAuthor
|
||||
});
|
||||
console.log(`✅ ${issueAuthor} is an organization member`);
|
||||
return; // Authorized
|
||||
} catch (error) {
|
||||
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
|
||||
}
|
||||
|
||||
// Close the issue with a comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
|
||||
`Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` +
|
||||
`If you would like to:\n` +
|
||||
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` +
|
||||
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
|
||||
`If you believe you should have access to create Task issues, please contact the maintainers.`
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
// Add a label to indicate this was auto-closed
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['auto-closed']
|
||||
});
|
@@ -99,7 +99,7 @@
|
||||
"barcode-detector": "3.0.5",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.43.0",
|
||||
"core-js": "3.44.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
@@ -136,7 +136,7 @@
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.4",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-data": "7.1.10",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -158,7 +158,7 @@
|
||||
"@octokit/auth-oauth-device": "8.0.1",
|
||||
"@octokit/plugin-retry": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.1.7",
|
||||
"@rsdoctor/rspack-plugin": "1.1.8",
|
||||
"@rspack/cli": "1.4.4",
|
||||
"@rspack/core": "1.4.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
|
@@ -31,7 +31,8 @@ export type LocalizeKeys =
|
||||
| `ui.panel.lovelace.card.${string}`
|
||||
| `ui.panel.lovelace.editor.${string}`
|
||||
| `ui.panel.page-authorize.form.${string}`
|
||||
| `component.${string}`;
|
||||
| `component.${string}`
|
||||
| `ui.entity.${string}`;
|
||||
|
||||
export type LandingPageKeys = FlattenObjectKeys<
|
||||
TranslationDict["landing-page"]
|
||||
|
@@ -72,6 +72,9 @@ export class HaControlButton extends LitElement {
|
||||
color 180ms ease-in-out;
|
||||
color: var(--control-button-icon-color);
|
||||
}
|
||||
:host([vertical]) .button {
|
||||
flex-direction: column;
|
||||
}
|
||||
.button:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-button-focus-color);
|
||||
}
|
||||
|
@@ -433,6 +433,7 @@ export class HaFloorPicker extends LitElement {
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._setValue(value);
|
||||
|
@@ -36,6 +36,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const PROGRAMMITIC_FIT_DELAY = 250;
|
||||
|
||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
||||
typeof entity === "string" ? entity : entity.entity_id;
|
||||
|
||||
@@ -113,14 +115,33 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _clickCount = 0;
|
||||
|
||||
private _isProgrammaticFit = false;
|
||||
|
||||
private _pauseAutoFit = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
this._pauseAutoFit = false;
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._handleVisibilityChange();
|
||||
super.connectedCallback();
|
||||
this._loadMap();
|
||||
this._attachObserver();
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = async () => {
|
||||
if (!document.hidden) {
|
||||
setTimeout(() => {
|
||||
this._pauseAutoFit = false;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
if (this.leafletMap) {
|
||||
this.leafletMap.remove();
|
||||
this.leafletMap = undefined;
|
||||
@@ -145,7 +166,7 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("entities")) {
|
||||
this._drawEntities();
|
||||
autoFitRequired = true;
|
||||
autoFitRequired = !this._pauseAutoFit;
|
||||
} else if (this._loaded && oldHass && this.entities) {
|
||||
// Check if any state has changed
|
||||
for (const entity of this.entities) {
|
||||
@@ -154,7 +175,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.hass!.states[getEntityId(entity)]
|
||||
) {
|
||||
this._drawEntities();
|
||||
autoFitRequired = true;
|
||||
autoFitRequired = !this._pauseAutoFit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -178,7 +199,11 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
if (changedProps.has("zoom")) {
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap!.setZoom(this.zoom);
|
||||
setTimeout(() => {
|
||||
this._isProgrammaticFit = false;
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -234,13 +259,30 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
this._clickCount++;
|
||||
});
|
||||
this.leafletMap.on("zoomstart", () => {
|
||||
if (!this._isProgrammaticFit) {
|
||||
this._pauseAutoFit = true;
|
||||
}
|
||||
});
|
||||
this.leafletMap.on("movestart", () => {
|
||||
if (!this._isProgrammaticFit) {
|
||||
this._pauseAutoFit = true;
|
||||
}
|
||||
});
|
||||
this._loaded = true;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public fitMap(options?: { zoom?: number; pad?: number }): void {
|
||||
public fitMap(options?: {
|
||||
zoom?: number;
|
||||
pad?: number;
|
||||
unpause_autofit?: boolean;
|
||||
}): void {
|
||||
if (options?.unpause_autofit) {
|
||||
this._pauseAutoFit = false;
|
||||
}
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
return;
|
||||
}
|
||||
@@ -250,6 +292,7 @@ export class HaMap extends ReactiveElement {
|
||||
!this._mapFocusZones.length &&
|
||||
!this.layers?.length
|
||||
) {
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap.setView(
|
||||
new this.Leaflet.LatLng(
|
||||
this.hass.config.latitude,
|
||||
@@ -257,6 +300,9 @@ export class HaMap extends ReactiveElement {
|
||||
),
|
||||
options?.zoom || this.zoom
|
||||
);
|
||||
setTimeout(() => {
|
||||
this._isProgrammaticFit = false;
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,8 +323,11 @@ export class HaMap extends ReactiveElement {
|
||||
});
|
||||
|
||||
bounds = bounds.pad(options?.pad ?? 0.5);
|
||||
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom });
|
||||
setTimeout(() => {
|
||||
this._isProgrammaticFit = false;
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
public fitBounds(
|
||||
@@ -291,7 +340,11 @@ export class HaMap extends ReactiveElement {
|
||||
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
|
||||
options?.pad ?? 0.5
|
||||
);
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom });
|
||||
setTimeout(() => {
|
||||
this._isProgrammaticFit = false;
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
private _drawLayers(prevLayers: Layer[] | undefined): void {
|
||||
|
@@ -1109,21 +1109,31 @@ export const computeConsumptionSingle = (data: {
|
||||
export const formatConsumptionShort = (
|
||||
hass: HomeAssistant,
|
||||
consumption: number | null,
|
||||
unit: string
|
||||
unit: string,
|
||||
targetUnit?: string
|
||||
): string => {
|
||||
if (!consumption) {
|
||||
return `0 ${unit}`;
|
||||
}
|
||||
const units = ["Wh", "kWh", "MWh", "GWh", "TWh"];
|
||||
let pickedUnit = unit;
|
||||
let val = consumption;
|
||||
let val = consumption || 0;
|
||||
let targetUnitIndex = -1;
|
||||
if (targetUnit) {
|
||||
targetUnitIndex = units.findIndex((u) => u === targetUnit);
|
||||
}
|
||||
let unitIndex = units.findIndex((u) => u === unit);
|
||||
if (unitIndex >= 0) {
|
||||
while (Math.abs(val) < 1 && unitIndex > 0) {
|
||||
while (
|
||||
targetUnitIndex > -1
|
||||
? targetUnitIndex < unitIndex
|
||||
: Math.abs(val) < 1 && unitIndex > 0
|
||||
) {
|
||||
val *= 1000;
|
||||
unitIndex--;
|
||||
}
|
||||
while (Math.abs(val) >= 1000 && unitIndex < units.length - 1) {
|
||||
while (
|
||||
targetUnitIndex > -1
|
||||
? targetUnitIndex > unitIndex
|
||||
: Math.abs(val) >= 1000 && unitIndex < units.length - 1
|
||||
) {
|
||||
val /= 1000;
|
||||
unitIndex++;
|
||||
}
|
||||
|
@@ -438,7 +438,10 @@ class DataEntryFlowDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = "loading_step";
|
||||
const delayedLoading = setTimeout(() => {
|
||||
// only show loading for slow steps to avoid flickering
|
||||
this._loading = "loading_step";
|
||||
}, 250);
|
||||
let _step: DataEntryFlowStep;
|
||||
try {
|
||||
_step = await step;
|
||||
@@ -452,6 +455,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
clearTimeout(delayedLoading);
|
||||
this._loading = undefined;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,272 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import { computeGroupEntitiesState } from "../../../../common/entity/group_entities";
|
||||
import "../../../../components/ha-control-button";
|
||||
import "../../../../components/ha-control-button-group";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import { isFullyClosed, isFullyOpen } from "../../../../data/cover";
|
||||
import { OFF, ON, UNAVAILABLE } from "../../../../data/entity";
|
||||
import { forwardHaptic } from "../../../../data/haptics";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { TileCardConfig } from "../../../../panels/lovelace/cards/types";
|
||||
import "../../../../panels/lovelace/sections/hui-section";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-more-info-state-header";
|
||||
|
||||
export interface GroupToggleDialogParams {
|
||||
entityIds: string[];
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-view-toggle-group")
|
||||
class HaMoreInfoViewToggleGroup extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public params?: GroupToggleDialogParams;
|
||||
|
||||
private _sectionConfig = memoizeOne(
|
||||
(entities: string[]): LovelaceSectionConfig => ({
|
||||
type: "grid",
|
||||
cards: entities.map<TileCardConfig>((entity) => ({
|
||||
type: "tile",
|
||||
entity: entity,
|
||||
icon_tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
private _combineEntities(entities: HassEntity[]): HassEntity {
|
||||
const firstEntity = entities[0];
|
||||
const domain = computeStateDomain(firstEntity);
|
||||
|
||||
const combined: HassEntity = {
|
||||
entity_id: `${domain}.all_entities`,
|
||||
state: computeGroupEntitiesState(entities),
|
||||
attributes: {
|
||||
device_class: firstEntity.attributes.device_class,
|
||||
},
|
||||
last_changed: new Date(
|
||||
Math.max(...entities.map((e) => new Date(e.last_changed).getTime()))
|
||||
).toISOString(),
|
||||
last_updated: new Date(
|
||||
Math.max(...entities.map((e) => new Date(e.last_updated).getTime()))
|
||||
).toISOString(),
|
||||
context: {
|
||||
id: "",
|
||||
parent_id: "",
|
||||
user_id: "",
|
||||
},
|
||||
};
|
||||
return combined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const sectionConfig = this._sectionConfig(this.params.entityIds);
|
||||
|
||||
const entities = this.params.entityIds
|
||||
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
|
||||
const groupStateObj = this._combineEntities(entities);
|
||||
|
||||
const formattedGroupState = this.hass.formatEntityState(groupStateObj);
|
||||
|
||||
const domain = computeStateDomain(groupStateObj);
|
||||
|
||||
const deviceClass = groupStateObj.attributes.device_class;
|
||||
|
||||
const availableEntities = entities.filter(
|
||||
(entity) => entity.state !== UNAVAILABLE
|
||||
);
|
||||
|
||||
const ON_STATE = domain === "cover" ? "open" : ON;
|
||||
const OFF_STATE = domain === "cover" ? "closed" : OFF;
|
||||
|
||||
const isAllOn = availableEntities.every((entity) =>
|
||||
computeDomain(entity.entity_id) === "cover"
|
||||
? isFullyOpen(entity)
|
||||
: entity.state === ON_STATE
|
||||
);
|
||||
const isAllOff = availableEntities.every((entity) =>
|
||||
computeDomain(entity.entity_id) === "cover"
|
||||
? isFullyClosed(entity)
|
||||
: entity.state === OFF_STATE
|
||||
);
|
||||
|
||||
const isMultiple = this.params.entityIds.length > 1;
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-more-info-state-header
|
||||
.hass=${this.hass}
|
||||
.stateObj=${groupStateObj}
|
||||
.stateOverride=${formattedGroupState}
|
||||
></ha-more-info-state-header>
|
||||
<div class="main">
|
||||
<ha-control-button-group vertical>
|
||||
<ha-control-button
|
||||
vertical
|
||||
@click=${this._turnAllOn}
|
||||
.disabled=${isAllOn}
|
||||
>
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
.state=${ON_STATE}
|
||||
.deviceClass=${deviceClass}
|
||||
></ha-domain-icon>
|
||||
<p>
|
||||
${domain === "cover"
|
||||
? isMultiple
|
||||
? this.hass.localize("ui.card.cover.open_all")
|
||||
: this.hass.localize("ui.card.cover.open")
|
||||
: isMultiple
|
||||
? this.hass.localize("ui.card.common.turn_on_all")
|
||||
: this.hass.localize("ui.card.common.turn_on")}
|
||||
</p>
|
||||
</ha-control-button>
|
||||
<ha-control-button
|
||||
vertical
|
||||
@click=${this._turnAllOff}
|
||||
.disabled=${isAllOff}
|
||||
>
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
.state=${OFF_STATE}
|
||||
.deviceClass=${deviceClass}
|
||||
.icon=${domain === "light" ? "mdi:lightbulb-off" : undefined}
|
||||
></ha-domain-icon>
|
||||
|
||||
<p>
|
||||
${domain === "cover"
|
||||
? isMultiple
|
||||
? this.hass.localize("ui.card.cover.close_all")
|
||||
: this.hass.localize("ui.card.cover.close")
|
||||
: isMultiple
|
||||
? this.hass.localize("ui.card.common.turn_off_all")
|
||||
: this.hass.localize("ui.card.common.turn_off")}
|
||||
</p>
|
||||
</ha-control-button>
|
||||
</ha-control-button-group>
|
||||
</div>
|
||||
|
||||
<div class="entities">
|
||||
<hui-section
|
||||
.config=${sectionConfig}
|
||||
.hass=${this.hass}
|
||||
></hui-section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _turnAllOff() {
|
||||
if (!this.params) {
|
||||
return;
|
||||
}
|
||||
|
||||
forwardHaptic("light");
|
||||
const domain = computeDomain(this.params.entityIds[0]);
|
||||
if (domain === "cover") {
|
||||
this.hass.callService("cover", "close_cover", {
|
||||
entity_id: this.params.entityIds,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.callService("homeassistant", "turn_off", {
|
||||
entity_id: this.params.entityIds,
|
||||
});
|
||||
}
|
||||
|
||||
private _turnAllOn() {
|
||||
if (!this.params) {
|
||||
return;
|
||||
}
|
||||
|
||||
forwardHaptic("light");
|
||||
const domain = computeDomain(this.params.entityIds[0]);
|
||||
if (domain === "cover") {
|
||||
this.hass.callService("cover", "open_cover", {
|
||||
entity_id: this.params.entityIds,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.callService("homeassistant", "turn_on", {
|
||||
entity_id: this.params.entityIds,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
ha-more-info-state-header {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-control-button-group {
|
||||
--control-button-group-spacing: 12px;
|
||||
--control-button-group-thickness: 130px;
|
||||
}
|
||||
ha-control-button {
|
||||
--control-button-border-radius: 16px;
|
||||
--mdc-icon-size: 24px;
|
||||
--control-button-padding: 16px 8px;
|
||||
--control-button-background-opacity: 0.1;
|
||||
}
|
||||
ha-control-button p {
|
||||
margin: 0;
|
||||
}
|
||||
.entities {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 12px;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), 12px);
|
||||
}
|
||||
hui-section {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-view-toggle-group": HaMoreInfoViewToggleGroup;
|
||||
}
|
||||
}
|
@@ -8,9 +8,9 @@ export const showVoiceAssistantsView = (
|
||||
title: string
|
||||
): void => {
|
||||
fireEvent(element, "show-child-view", {
|
||||
viewTag: "ha-more-info-view-voice-assistants",
|
||||
viewImport: loadVoiceAssistantsView,
|
||||
viewTitle: title,
|
||||
viewParams: {},
|
||||
tag: "ha-more-info-view-voice-assistants",
|
||||
import: loadVoiceAssistantsView,
|
||||
title: title,
|
||||
params: {},
|
||||
});
|
||||
};
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
mdiPencilOutline,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
@@ -68,15 +68,24 @@ export interface MoreInfoDialogParams {
|
||||
view?: View;
|
||||
/** @deprecated Use `view` instead */
|
||||
tab?: View;
|
||||
parentView?: ParentView;
|
||||
}
|
||||
|
||||
type View = "info" | "history" | "settings" | "related";
|
||||
type View = "info" | "history" | "settings" | "related" | "parent";
|
||||
|
||||
interface ParentView {
|
||||
tag: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
import?: () => Promise<unknown>;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
interface ChildView {
|
||||
viewTag: string;
|
||||
viewTitle?: string;
|
||||
viewImport?: () => Promise<unknown>;
|
||||
viewParams?: any;
|
||||
tag: string;
|
||||
title?: string;
|
||||
import?: () => Promise<unknown>;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -88,7 +97,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_VIEW: View = "info";
|
||||
const INFO_VIEW: View = "info";
|
||||
const PARENT_VIEW: View = "parent";
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends LitElement {
|
||||
@@ -98,9 +108,11 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@state() private _entityId?: string | null;
|
||||
|
||||
@state() private _currView: View = DEFAULT_VIEW;
|
||||
@state() private _currView: View = INFO_VIEW;
|
||||
|
||||
@state() private _initialView: View = DEFAULT_VIEW;
|
||||
@state() private _initialView: View = INFO_VIEW;
|
||||
|
||||
@state() private _parentView?: ParentView;
|
||||
|
||||
@state() private _childView?: ChildView;
|
||||
|
||||
@@ -114,17 +126,29 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
public showDialog(params: MoreInfoDialogParams) {
|
||||
this._entityId = params.entityId;
|
||||
if (!this._entityId) {
|
||||
if (!this._entityId && !params.parentView) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._currView = params.view || DEFAULT_VIEW;
|
||||
this._initialView = params.view || DEFAULT_VIEW;
|
||||
this._parentView = params.parentView;
|
||||
if (this._parentView?.import) {
|
||||
this._parentView.import();
|
||||
this._currView = PARENT_VIEW;
|
||||
} else {
|
||||
this._currView = params.view || INFO_VIEW;
|
||||
}
|
||||
this._initialView = params.view || INFO_VIEW;
|
||||
this._childView = undefined;
|
||||
this.large = false;
|
||||
this._loadEntityRegistryEntry();
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("_entityId")) {
|
||||
this._loadEntityRegistryEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadEntityRegistryEntry() {
|
||||
if (!this._entityId) {
|
||||
return;
|
||||
@@ -143,19 +167,18 @@ export class MoreInfoDialog extends LitElement {
|
||||
this._entityId = undefined;
|
||||
this._entry = undefined;
|
||||
this._childView = undefined;
|
||||
this._parentView = undefined;
|
||||
this._currView = INFO_VIEW;
|
||||
this._infoEditMode = false;
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
this._initialView = INFO_VIEW;
|
||||
this._isEscapeEnabled = true;
|
||||
window.removeEventListener("dialog-closed", this._enableEscapeKeyClose);
|
||||
window.removeEventListener("show-dialog", this._disableEscapeKeyClose);
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _shouldShowEditIcon(
|
||||
domain: string,
|
||||
stateObj: HassEntity | undefined
|
||||
): boolean {
|
||||
if (__DEMO__ || !stateObj) {
|
||||
private _shouldShowEditIcon(domain?: string, stateObj?: HassEntity): boolean {
|
||||
if (__DEMO__ || !stateObj || !domain) {
|
||||
return false;
|
||||
}
|
||||
if (EDITABLE_DOMAINS_WITH_ID.includes(domain) && stateObj.attributes.id) {
|
||||
@@ -171,8 +194,9 @@ export class MoreInfoDialog extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
private _shouldShowHistory(domain: string): boolean {
|
||||
private _shouldShowHistory(domain?: string): boolean {
|
||||
return (
|
||||
domain !== undefined &&
|
||||
DOMAINS_WITH_MORE_INFO.includes(domain) &&
|
||||
(computeShowHistoryComponent(this.hass, this._entityId!) ||
|
||||
computeShowLogBookComponent(
|
||||
@@ -207,14 +231,30 @@ export class MoreInfoDialog extends LitElement {
|
||||
private _goBack() {
|
||||
if (this._childView) {
|
||||
this._childView = undefined;
|
||||
} else {
|
||||
this._setView(this._initialView);
|
||||
return;
|
||||
}
|
||||
const previousView = this._previousView();
|
||||
if (previousView) {
|
||||
this._setView(previousView);
|
||||
}
|
||||
}
|
||||
|
||||
private _previousView(): View | undefined {
|
||||
if (this._currView === PARENT_VIEW) {
|
||||
return undefined;
|
||||
}
|
||||
if (this._currView !== this._initialView) {
|
||||
return this._initialView;
|
||||
}
|
||||
if (this._parentView) {
|
||||
return PARENT_VIEW;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _resetInitialView() {
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
this._setView(DEFAULT_VIEW);
|
||||
this._initialView = INFO_VIEW;
|
||||
this._setView(INFO_VIEW);
|
||||
}
|
||||
|
||||
private _goToHistory() {
|
||||
@@ -227,8 +267,8 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
private _showChildView(ev: CustomEvent): void {
|
||||
const view = ev.detail as ChildView;
|
||||
if (view.viewImport) {
|
||||
view.viewImport();
|
||||
if (view.import) {
|
||||
view.import();
|
||||
}
|
||||
this._childView = view;
|
||||
}
|
||||
@@ -286,45 +326,99 @@ export class MoreInfoDialog extends LitElement {
|
||||
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
|
||||
}
|
||||
|
||||
private _handleMoreInfoEvent(ev: CustomEvent) {
|
||||
// If the parent view has a `show-dialog` event to open more info, we handle it here to set the entity ID and view.
|
||||
const detail = ev.detail as MoreInfoDialogParams;
|
||||
if (detail.entityId) {
|
||||
this._entityId = detail.entityId;
|
||||
this._setView(detail.view || INFO_VIEW);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderHeader = (): TemplateResult | typeof nothing => {
|
||||
if (this._parentView && this._currView === PARENT_VIEW) {
|
||||
return html`
|
||||
${this._parentView
|
||||
? html`<p class="breadcrumb">${this._parentView.subtitle}</p>`
|
||||
: nothing}
|
||||
<p class="main">${this._parentView.title}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
const entityId = this._entityId;
|
||||
|
||||
if (entityId) {
|
||||
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||
const context = stateObj
|
||||
? getEntityContext(stateObj, this.hass)
|
||||
: this._entry
|
||||
? getEntityEntryContext(this._entry, this.hass)
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const areaName = context?.area
|
||||
? computeAreaName(context.area)
|
||||
: undefined;
|
||||
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const title = this._childView?.title || breadcrumb.pop() || entityId;
|
||||
const isAdmin = this.hass.user!.is_admin;
|
||||
|
||||
return html`
|
||||
${breadcrumb.length > 0
|
||||
? !__DEMO__ && isAdmin
|
||||
? html`
|
||||
<button
|
||||
class="breadcrumb"
|
||||
@click=${this._breadcrumbClick}
|
||||
aria-label=${breadcrumb.join(" > ")}
|
||||
>
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<p class="breadcrumb">
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
<p class="main">${title}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this._entityId) {
|
||||
if (!this._entityId && !this._parentView) {
|
||||
return nothing;
|
||||
}
|
||||
const entityId = this._entityId;
|
||||
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||
|
||||
const isAdmin = this.hass.user!.is_admin;
|
||||
|
||||
const deviceId = this._getDeviceId();
|
||||
|
||||
const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView;
|
||||
const previousView = this._previousView();
|
||||
const isDefaultView = this._currView === INFO_VIEW && !this._childView;
|
||||
const isSpecificInitialView =
|
||||
this._initialView !== DEFAULT_VIEW && !this._childView;
|
||||
const showCloseIcon = isDefaultView || isSpecificInitialView;
|
||||
|
||||
const context = stateObj
|
||||
? getEntityContext(stateObj, this.hass)
|
||||
: this._entry
|
||||
? getEntityEntryContext(this._entry, this.hass)
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const areaName = context?.area ? computeAreaName(context.area) : undefined;
|
||||
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
|
||||
this._initialView !== INFO_VIEW && !this._childView;
|
||||
const showCloseIcon = !previousView && !this._childView;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -332,7 +426,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
@closed=${this.closeDialog}
|
||||
@opened=${this._handleOpened}
|
||||
.escapeKeyAction=${this._isEscapeEnabled ? undefined : ""}
|
||||
.heading=${title}
|
||||
.heading=${" "}
|
||||
hideActions
|
||||
flexContent
|
||||
>
|
||||
@@ -356,24 +450,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
></ha-icon-button-prev>
|
||||
`}
|
||||
<span slot="title" @click=${this._enlarge} class="title">
|
||||
${breadcrumb.length > 0
|
||||
? !__DEMO__ && isAdmin
|
||||
? html`
|
||||
<button
|
||||
class="breadcrumb"
|
||||
@click=${this._breadcrumbClick}
|
||||
aria-label=${breadcrumb.join(" > ")}
|
||||
>
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<p class="breadcrumb">
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
<p class="main">${title}</p>
|
||||
${this._renderHeader()}
|
||||
</span>
|
||||
${isDefaultView
|
||||
? html`
|
||||
@@ -521,54 +598,62 @@ export class MoreInfoDialog extends LitElement {
|
||||
@show-child-view=${this._showChildView}
|
||||
@entity-entry-updated=${this._entryUpdated}
|
||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||
@hass-more-info=${this._handleMoreInfoEvent}
|
||||
>
|
||||
${cache(
|
||||
this._childView
|
||||
? html`
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
${dynamicElement(this._childView.tag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
params: this._childView.params,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "history"
|
||||
: this._currView === "parent"
|
||||
? dynamicElement(this._parentView!.tag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._parentView!.params,
|
||||
})
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-history-and-logbook
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history-and-logbook>
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "settings"
|
||||
: this._currView === "history"
|
||||
? html`
|
||||
<ha-more-info-settings
|
||||
<ha-more-info-history-and-logbook
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
></ha-more-info-history-and-logbook>
|
||||
`
|
||||
: this._currView === "related"
|
||||
: this._currView === "settings"
|
||||
? html`
|
||||
<ha-related-items
|
||||
<ha-more-info-settings
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
`
|
||||
: nothing
|
||||
: this._currView === "related"
|
||||
? html`
|
||||
<ha-related-items
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${domain &&
|
||||
SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
|
@@ -19,6 +19,7 @@ import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeFloorName } from "../../../common/entity/compute_floor_name";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
@@ -424,6 +425,18 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
);
|
||||
|
||||
let floorName = "—";
|
||||
if (
|
||||
device.area_id &&
|
||||
areas[device.area_id]?.floor_id &&
|
||||
this.hass.floors
|
||||
) {
|
||||
const floorId = areas[device.area_id].floor_id;
|
||||
if (this.hass.floors[floorId!]) {
|
||||
floorName = computeFloorName(this.hass.floors[floorId!]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...device,
|
||||
name: computeDeviceNameDisplay(
|
||||
@@ -441,6 +454,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
device.area_id && areas[device.area_id]
|
||||
? areas[device.area_id].name
|
||||
: "—",
|
||||
floor: floorName,
|
||||
integration: deviceEntries.length
|
||||
? deviceEntries
|
||||
.map(
|
||||
@@ -524,6 +538,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
groupable: true,
|
||||
minWidth: "120px",
|
||||
},
|
||||
floor: {
|
||||
title: localize("ui.panel.config.devices.data_table.floor"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
minWidth: "120px",
|
||||
defaultHidden: true,
|
||||
},
|
||||
integration: {
|
||||
title: localize("ui.panel.config.devices.data_table.integration"),
|
||||
sortable: true,
|
||||
|
@@ -15,13 +15,11 @@ import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { domainIcon } from "../../../data/icons";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { GroupToggleDialogParams } from "../../../dialogs/more-info/components/voice/ha-more-info-view-toggle-group";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { computeCssVariable } from "../../../resources/css-variables";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { AreaCardFeatureContext } from "../cards/hui-area-card";
|
||||
import type { ButtonCardConfig, TileCardConfig } from "../cards/types";
|
||||
import { showDashboardDialog } from "../dialogs/show-dashboard-dialog";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
@@ -33,39 +31,24 @@ import type {
|
||||
import { AREA_CONTROLS } from "./types";
|
||||
|
||||
interface AreaControlsButton {
|
||||
offIcon?: string;
|
||||
onIcon?: string;
|
||||
filter: {
|
||||
domain: string;
|
||||
device_class?: string;
|
||||
};
|
||||
domain: string;
|
||||
device_class?: string;
|
||||
}
|
||||
|
||||
const coverButton = (deviceClass: string) => ({
|
||||
filter: {
|
||||
domain: "cover",
|
||||
device_class: deviceClass,
|
||||
},
|
||||
domain: "cover",
|
||||
device_class: deviceClass,
|
||||
});
|
||||
|
||||
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
||||
light: {
|
||||
// Overrides the icons for lights
|
||||
offIcon: "mdi:lightbulb-off",
|
||||
onIcon: "mdi:lightbulb",
|
||||
filter: {
|
||||
domain: "light",
|
||||
},
|
||||
domain: "light",
|
||||
},
|
||||
fan: {
|
||||
filter: {
|
||||
domain: "fan",
|
||||
},
|
||||
domain: "fan",
|
||||
},
|
||||
switch: {
|
||||
filter: {
|
||||
domain: "switch",
|
||||
},
|
||||
domain: "switch",
|
||||
},
|
||||
"cover-blind": coverButton("blind"),
|
||||
"cover-curtain": coverButton("curtain"),
|
||||
@@ -99,7 +82,8 @@ export const getAreaControlEntities = (
|
||||
const filter = generateEntityFilter(hass, {
|
||||
area: areaId,
|
||||
entity_category: "none",
|
||||
...controlButton.filter,
|
||||
domain: controlButton.domain,
|
||||
device_class: controlButton.device_class,
|
||||
});
|
||||
|
||||
acc[control] = Object.keys(hass.entities).filter(
|
||||
@@ -177,98 +161,27 @@ class HuiAreaControlsCardFeature
|
||||
);
|
||||
const entitiesIds = controlEntities[control];
|
||||
|
||||
const entities = entitiesIds
|
||||
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
const { domain, device_class: dc } = AREA_CONTROLS_BUTTONS[control];
|
||||
|
||||
const controlButton = AREA_CONTROLS_BUTTONS[control];
|
||||
const onIcon =
|
||||
controlButton.onIcon ||
|
||||
(await domainIcon(
|
||||
this.hass!,
|
||||
controlButton.filter.domain,
|
||||
controlButton.filter.device_class,
|
||||
"off"
|
||||
));
|
||||
const offIcon =
|
||||
controlButton.offIcon ||
|
||||
(await domainIcon(
|
||||
this.hass!,
|
||||
controlButton.filter.domain,
|
||||
controlButton.filter.device_class,
|
||||
"on"
|
||||
));
|
||||
const domainName = this.hass.localize(
|
||||
`component.${domain}.entity_component.${dc ?? "_"}.name`
|
||||
);
|
||||
|
||||
const sectionConfig: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: "Actions",
|
||||
heading_style: "subtitle",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
icon: offIcon,
|
||||
icon_height: "24px",
|
||||
name: "Turn all off",
|
||||
tap_action: {
|
||||
action: "perform-action",
|
||||
target: {
|
||||
entity_id: entitiesIds,
|
||||
},
|
||||
perform_action: "light.turn_off",
|
||||
},
|
||||
grid_options: {
|
||||
min_rows: 1,
|
||||
rows: 1,
|
||||
columns: 6,
|
||||
},
|
||||
} as ButtonCardConfig,
|
||||
{
|
||||
type: "button",
|
||||
icon: onIcon,
|
||||
icon_height: "24px",
|
||||
name: "Turn all on",
|
||||
tap_action: {
|
||||
action: "perform-action",
|
||||
target: {
|
||||
entity_id: entitiesIds,
|
||||
},
|
||||
perform_action: "light.turn_on",
|
||||
},
|
||||
grid_options: {
|
||||
min_rows: 1,
|
||||
rows: 1,
|
||||
columns: 6,
|
||||
},
|
||||
} as ButtonCardConfig,
|
||||
{
|
||||
type: "heading",
|
||||
heading: "Controls",
|
||||
heading_style: "subtitle",
|
||||
},
|
||||
...entities.map<TileCardConfig>((entity) => ({
|
||||
type: "tile",
|
||||
entity: entity.entity_id,
|
||||
features_position: "inline",
|
||||
features: [
|
||||
{
|
||||
type: "light-brightness",
|
||||
},
|
||||
],
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
showDashboardDialog(this, {
|
||||
sections: [sectionConfig],
|
||||
title: computeAreaName(this._area!) || "",
|
||||
subtitle: control,
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: null,
|
||||
parentView: {
|
||||
title: computeAreaName(this._area!) || "",
|
||||
subtitle: domainName,
|
||||
tag: "ha-more-info-view-toggle-group",
|
||||
import: () =>
|
||||
import(
|
||||
"../../../dialogs/more-info/components/voice/ha-more-info-view-toggle-group"
|
||||
),
|
||||
params: {
|
||||
entityIds: entitiesIds,
|
||||
} as GroupToggleDialogParams,
|
||||
},
|
||||
});
|
||||
|
||||
// forwardHaptic("light");
|
||||
// toggleGroupEntities(this.hass, entities);
|
||||
}
|
||||
|
||||
private _controlEntities = memoizeOne(
|
||||
@@ -339,15 +252,22 @@ class HuiAreaControlsCardFeature
|
||||
? stateActive(entities[0], groupState)
|
||||
: false;
|
||||
|
||||
const label = this.hass!.localize(
|
||||
`ui.card_features.area_controls.${control}.${active ? "off" : "on"}`
|
||||
const domain = button.domain;
|
||||
const dc = button.device_class;
|
||||
|
||||
const domainName = this.hass!.localize(
|
||||
`component.${domain}.entity_component.${dc ?? "_"}.name`
|
||||
);
|
||||
|
||||
const icon = active ? button.onIcon : button.offIcon;
|
||||
const label = `${domainName}: ${this.hass!.localize(
|
||||
`ui.card_features.area_controls.open_more_info`
|
||||
)}`;
|
||||
|
||||
const domain = button.filter.domain;
|
||||
const deviceClass = button.filter.device_class
|
||||
? ensureArray(button.filter.device_class)[0]
|
||||
const icon =
|
||||
domain === "light" && !active ? "mdi:lightbulb-off" : undefined;
|
||||
|
||||
const deviceClass = button.device_class
|
||||
? ensureArray(button.device_class)[0]
|
||||
: undefined;
|
||||
|
||||
const activeColor = computeCssVariable(
|
||||
|
@@ -255,6 +255,20 @@ class HuiEnergyDistrubutionCard
|
||||
(batteryFromGrid || 0) +
|
||||
(batteryToGrid || 0);
|
||||
|
||||
// Coerce all energy numbers to the same unit (the biggest)
|
||||
const maxEnergy = Math.max(
|
||||
lowCarbonEnergy || 0,
|
||||
totalSolarProduction || 0,
|
||||
returnedToGrid || 0,
|
||||
totalFromGrid || 0,
|
||||
totalHomeConsumption,
|
||||
totalBatteryIn || 0,
|
||||
totalBatteryOut || 0
|
||||
);
|
||||
const targetEnergyUnit = formatConsumptionShort(this.hass, maxEnergy, "kWh")
|
||||
.split(" ")
|
||||
.pop();
|
||||
|
||||
return html`
|
||||
<ha-card .header=${this._config.title}>
|
||||
<div class="card-content">
|
||||
@@ -281,7 +295,8 @@ class HuiEnergyDistrubutionCard
|
||||
${formatConsumptionShort(
|
||||
this.hass,
|
||||
lowCarbonEnergy,
|
||||
"kWh"
|
||||
"kWh",
|
||||
targetEnergyUnit
|
||||
)}
|
||||
</a>
|
||||
<svg width="80" height="30">
|
||||
@@ -300,7 +315,8 @@ class HuiEnergyDistrubutionCard
|
||||
${formatConsumptionShort(
|
||||
this.hass,
|
||||
totalSolarProduction,
|
||||
"kWh"
|
||||
"kWh",
|
||||
targetEnergyUnit
|
||||
)}
|
||||
</div>
|
||||
</div>`
|
||||
@@ -396,7 +412,8 @@ class HuiEnergyDistrubutionCard
|
||||
>${formatConsumptionShort(
|
||||
this.hass,
|
||||
returnedToGrid,
|
||||
"kWh"
|
||||
"kWh",
|
||||
targetEnergyUnit
|
||||
)}
|
||||
</span>`
|
||||
: ""}
|
||||
@@ -409,7 +426,8 @@ class HuiEnergyDistrubutionCard
|
||||
: ""}${formatConsumptionShort(
|
||||
this.hass,
|
||||
totalFromGrid,
|
||||
"kWh"
|
||||
"kWh",
|
||||
targetEnergyUnit
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -432,7 +450,8 @@ class HuiEnergyDistrubutionCard
|
||||
${formatConsumptionShort(
|
||||
this.hass,
|
||||
totalHomeConsumption,
|
||||
"kWh"
|
||||
"kWh",
|
||||
targetEnergyUnit
|
||||
)}
|
||||
${homeSolarCircumference !== undefined ||
|
||||
homeLowCarbonCircumference !== undefined
|
||||
@@ -535,7 +554,8 @@ class HuiEnergyDistrubutionCard
|
||||
>${formatConsumptionShort(
|
||||
this.hass,
|
||||
totalBatteryIn,
|
||||
"kWh"
|
||||
"kWh",
|
||||
targetEnergyUnit
|
||||
)}
|
||||
</span>
|
||||
<span class="battery-out">
|
||||
@@ -546,7 +566,8 @@ class HuiEnergyDistrubutionCard
|
||||
>${formatConsumptionShort(
|
||||
this.hass,
|
||||
totalBatteryOut,
|
||||
"kWh"
|
||||
"kWh",
|
||||
targetEnergyUnit
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
@@ -239,7 +239,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
)}
|
||||
.path=${mdiImageFilterCenterFocus}
|
||||
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
|
||||
@click=${this._fitMap}
|
||||
@click=${this._resetFocus}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
@@ -389,8 +389,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
: (root.style.paddingBottom = "100%");
|
||||
}
|
||||
|
||||
private _fitMap() {
|
||||
this._map?.fitMap();
|
||||
private _resetFocus() {
|
||||
this._map?.fitMap({ unpause_autofit: true });
|
||||
}
|
||||
|
||||
private _toggleClusterMarkers() {
|
||||
|
@@ -464,10 +464,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
if (this._config?.show_forecast !== false) {
|
||||
rows += 1;
|
||||
min_rows += 1;
|
||||
if (this._config?.forecast_type === "daily") {
|
||||
rows += 1;
|
||||
}
|
||||
}
|
||||
if (this._config?.forecast_type === "daily") {
|
||||
rows += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
columns: 12,
|
||||
rows: rows,
|
||||
|
@@ -1,85 +0,0 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "../../../components/ha-header-bar";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../sections/hui-section";
|
||||
import type { DashboardDialogParams } from "./show-dashboard-dialog";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
|
||||
@customElement("hui-dialog-dashboard")
|
||||
class HuiDashboardDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DashboardDialogParams;
|
||||
|
||||
public async showDialog(params: DashboardDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this._params.title}
|
||||
hideActions
|
||||
flexContent
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${this._params.title}</span>
|
||||
${this._params.subtitle
|
||||
? html`<span slot="subtitle">${this._params.subtitle}</span>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div class="content">
|
||||
${repeat(
|
||||
this._params.sections,
|
||||
(section) => section,
|
||||
(section: LovelaceSectionConfig) => html`
|
||||
<hui-section .config=${section} .hass=${this.hass}></hui-section>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-dashboard": HuiDashboardDialog;
|
||||
}
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
|
||||
export interface DashboardDialogParams {
|
||||
sections: LovelaceSectionConfig[];
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export const showDashboardDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: DashboardDialogParams
|
||||
) => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-dashboard",
|
||||
dialogImport: () => import("./hui-dialog-dashboard"),
|
||||
dialogParams: dialogParams,
|
||||
});
|
||||
};
|
@@ -57,7 +57,7 @@ export class HuiWeatherForecastCardEditor
|
||||
|
||||
if (
|
||||
/* cannot show forecast in case it is unavailable on the entity */
|
||||
(config.show_forecast === true && this._hasForecast === false) ||
|
||||
(config.show_forecast !== false && this._hasForecast === false) ||
|
||||
/* cannot hide both weather and forecast, need one of them */
|
||||
(config.show_current === false && config.show_forecast === false)
|
||||
) {
|
||||
@@ -65,6 +65,7 @@ export class HuiWeatherForecastCardEditor
|
||||
fireEvent(this, "config-changed", {
|
||||
config: { ...config, show_current: true, show_forecast: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!config.forecast_type ||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
@@ -148,7 +149,22 @@ export class AreaViewStrategy extends ReactiveElement {
|
||||
hass.localize("ui.panel.lovelace.strategy.areas.groups.security"),
|
||||
AREA_STRATEGY_GROUP_ICONS.security
|
||||
),
|
||||
...security.map(computeTileCard),
|
||||
...security.map((entityId) => {
|
||||
const domain = computeDomain(entityId);
|
||||
if (domain === "camera") {
|
||||
return {
|
||||
type: "picture-entity",
|
||||
entity: entityId,
|
||||
show_state: false,
|
||||
show_name: false,
|
||||
grid_options: {
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
},
|
||||
};
|
||||
}
|
||||
return computeTileCard(entityId);
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../../../common/entity/compute_state_name";
|
||||
import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter";
|
||||
import { generateEntityFilter } from "../../../../../common/entity/entity_filter";
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
import type { AreaRegistryEntry } from "../../../../../data/area_registry";
|
||||
import { areaCompare } from "../../../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../../data/floor_registry";
|
||||
import type { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supportsAlarmModesCardFeature } from "../../../card-features/hui-alarm-modes-card-feature";
|
||||
import { supportsCoverOpenCloseCardFeature } from "../../../card-features/hui-cover-open-close-card-feature";
|
||||
@@ -210,7 +208,7 @@ export const getAreaGroupedEntities = (
|
||||
|
||||
export const computeAreaTileCardConfig =
|
||||
(hass: HomeAssistant, prefix: string, includeFeature?: boolean) =>
|
||||
(entity: string): LovelaceCardConfig => {
|
||||
(entity: string): TileCardConfig => {
|
||||
const stateObj = hass.states[entity];
|
||||
|
||||
const context: LovelaceCardFeatureContext = {
|
||||
@@ -219,21 +217,6 @@ export const computeAreaTileCardConfig =
|
||||
|
||||
const additionalCardConfig: Partial<TileCardConfig> = {};
|
||||
|
||||
const domain = computeDomain(entity);
|
||||
|
||||
if (domain === "camera") {
|
||||
return {
|
||||
type: "picture-entity",
|
||||
entity: entity,
|
||||
show_state: false,
|
||||
show_name: false,
|
||||
grid_options: {
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let feature: LovelaceCardFeatureConfig | undefined;
|
||||
if (includeFeature) {
|
||||
if (supportsLightBrightnessCardFeature(hass, context)) {
|
||||
|
@@ -30,6 +30,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
{
|
||||
entityId: ev.detail.entityId,
|
||||
view: ev.detail.view || ev.detail.tab,
|
||||
parentView: ev.detail.parentView,
|
||||
},
|
||||
() => import("../dialogs/more-info/ha-more-info-dialog")
|
||||
);
|
||||
|
@@ -79,6 +79,8 @@
|
||||
"common": {
|
||||
"turn_on": "Turn on",
|
||||
"turn_off": "Turn off",
|
||||
"turn_on_all": "Turn on all",
|
||||
"turn_off_all": "Turn off all",
|
||||
"toggle": "Toggle",
|
||||
"entity_not_found": "Entity not found"
|
||||
},
|
||||
@@ -145,7 +147,11 @@
|
||||
"close_cover": "Close cover",
|
||||
"open_tilt_cover": "Open cover tilt",
|
||||
"close_tilt_cover": "Close cover tilt",
|
||||
"stop_cover": "Stop cover"
|
||||
"stop_cover": "Stop cover",
|
||||
"open": "Open",
|
||||
"open_all": "Open all",
|
||||
"close": "Close",
|
||||
"close_all": "Close all"
|
||||
},
|
||||
"fan": {
|
||||
"preset_mode": "Preset mode",
|
||||
@@ -327,58 +333,7 @@
|
||||
},
|
||||
"card_features": {
|
||||
"area_controls": {
|
||||
"light": {
|
||||
"on": "Turn on area lights",
|
||||
"off": "Turn off area lights"
|
||||
},
|
||||
"fan": {
|
||||
"on": "Turn on area fans",
|
||||
"off": "Turn off area fans"
|
||||
},
|
||||
"switch": {
|
||||
"on": "Turn on area switches",
|
||||
"off": "Turn off area switches"
|
||||
},
|
||||
"cover-awning": {
|
||||
"on": "Open area awnings",
|
||||
"off": "Close area awnings"
|
||||
},
|
||||
"cover-blind": {
|
||||
"on": "Open area blinds",
|
||||
"off": "Close area blinds"
|
||||
},
|
||||
"cover-curtain": {
|
||||
"on": "Open area curtains",
|
||||
"off": "Close area curtains"
|
||||
},
|
||||
"cover-damper": {
|
||||
"on": "Open area dampers",
|
||||
"off": "Close area dampers"
|
||||
},
|
||||
"cover-door": {
|
||||
"on": "Open area doors",
|
||||
"off": "Close area doors"
|
||||
},
|
||||
"cover-garage": {
|
||||
"on": "Open garage door",
|
||||
"off": "Close garage door"
|
||||
},
|
||||
"cover-gate": {
|
||||
"on": "Open area gates",
|
||||
"off": "Close area gates"
|
||||
},
|
||||
"cover-shade": {
|
||||
"on": "Open area shades",
|
||||
"off": "Close area shades"
|
||||
},
|
||||
"cover-shutter": {
|
||||
"on": "Open area shutters",
|
||||
"off": "Close area shutters"
|
||||
},
|
||||
"cover-window": {
|
||||
"on": "Open area windows",
|
||||
"off": "Close area windows"
|
||||
}
|
||||
"open_more_info": "Open more info"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -5138,6 +5093,7 @@
|
||||
"manufacturer": "Manufacturer",
|
||||
"model": "Model",
|
||||
"area": "Area",
|
||||
"floor": "Floor",
|
||||
"integration": "Integration",
|
||||
"battery": "Battery",
|
||||
"disabled_by": "Disabled",
|
||||
@@ -5671,7 +5627,7 @@
|
||||
"other_networks": "Other networks",
|
||||
"my_network": "Preferred network",
|
||||
"no_preferred_network": "You don't have a preferred network yet.",
|
||||
"more_info": "More Info",
|
||||
"more_info": "More information",
|
||||
"add_open_thread_border_router": "Add an OpenThread border router",
|
||||
"reset_border_router": "Reset border router",
|
||||
"add_to_my_network": "Add to preferred network",
|
||||
@@ -8510,7 +8466,7 @@
|
||||
"filter_states": "Filter states",
|
||||
"filter_attributes": "Filter attributes",
|
||||
"no_entities": "No entities",
|
||||
"more_info": "More Info",
|
||||
"more_info": "More info",
|
||||
"alert_entity_field": "Entity is a mandatory field",
|
||||
"last_updated": "[%key:ui::dialogs::more_info_control::last_updated%]",
|
||||
"last_changed": "[%key:ui::dialogs::more_info_control::last_changed%]",
|
||||
@@ -8670,7 +8626,7 @@
|
||||
"input_select": "Input selects",
|
||||
"template": "Template entities",
|
||||
"universal": "Universal media player entities",
|
||||
"rest": "Rest entities and notify services",
|
||||
"rest": "REST entities and notify services",
|
||||
"command_line": "Command line entities",
|
||||
"filter": "Filter entities",
|
||||
"statistics": "Statistics entities",
|
||||
@@ -9106,7 +9062,7 @@
|
||||
},
|
||||
"host_pid": {
|
||||
"title": "Host processes namespace",
|
||||
"description": "Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on."
|
||||
"description": "Usually, the processes run by the add-on are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on."
|
||||
},
|
||||
"apparmor": {
|
||||
"title": "AppArmor",
|
||||
|
48
test/common/url/search-params.test.ts
Normal file
48
test/common/url/search-params.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it, vi, afterEach } from "vitest";
|
||||
|
||||
import {
|
||||
addSearchParam,
|
||||
createSearchParam,
|
||||
extractSearchParam,
|
||||
extractSearchParamsObject,
|
||||
removeSearchParam,
|
||||
} from "../../../src/common/url/search-params";
|
||||
|
||||
const sortQueryString = (querystring: string): string =>
|
||||
querystring.split("&").sort().join("&");
|
||||
|
||||
vi.mock("../../../src/common/dom/get_main_window", () => ({
|
||||
mainWindow: { location: { search: "?param1=ab+c¶m2" } },
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("Search Params Tests", () => {
|
||||
it("should extract all search params from window object", () => {
|
||||
expect(extractSearchParamsObject()).toEqual({ param1: "ab c", param2: "" });
|
||||
});
|
||||
|
||||
it("should return value for specified search param from window object", () => {
|
||||
expect(extractSearchParam("param1")).toEqual("ab c");
|
||||
});
|
||||
|
||||
it("should create query string from given object", () => {
|
||||
expect(
|
||||
sortQueryString(createSearchParam({ param1: "ab c", param2: "" }))
|
||||
).toEqual(sortQueryString("param1=ab+c¶m2="));
|
||||
});
|
||||
|
||||
it("should return query string which combines provided param object and window.location.search", () => {
|
||||
expect(
|
||||
sortQueryString(addSearchParam({ param4: "", param3: "x y" }))
|
||||
).toEqual(sortQueryString("param1=ab+c¶m2=¶m3=x+y¶m4="));
|
||||
});
|
||||
|
||||
it("should return query string from window.location.search but remove the provided param from it", () => {
|
||||
expect(sortQueryString(removeSearchParam("param2"))).toEqual(
|
||||
sortQueryString("param1=ab+c")
|
||||
);
|
||||
});
|
||||
});
|
@@ -70,8 +70,10 @@ describe("Energy Short Format Test", () => {
|
||||
const hass = { locale: defaultLocale } as HomeAssistant;
|
||||
it("No Unit conversion", () => {
|
||||
assert.strictEqual(formatConsumptionShort(hass, 0, "Wh"), "0 Wh");
|
||||
assert.strictEqual(formatConsumptionShort(hass, 0, "kWh"), "0 kWh");
|
||||
assert.strictEqual(formatConsumptionShort(hass, 0, "GWh"), "0 GWh");
|
||||
assert.strictEqual(formatConsumptionShort(hass, 0, "kWh"), "0 Wh");
|
||||
assert.strictEqual(formatConsumptionShort(hass, 0, "kWh", "kWh"), "0 kWh");
|
||||
assert.strictEqual(formatConsumptionShort(hass, 0, "GWh"), "0 Wh");
|
||||
assert.strictEqual(formatConsumptionShort(hass, 0, "GWh", "GWh"), "0 GWh");
|
||||
assert.strictEqual(formatConsumptionShort(hass, 0, "gal"), "0 gal");
|
||||
|
||||
assert.strictEqual(
|
||||
@@ -139,6 +141,36 @@ describe("Energy Short Format Test", () => {
|
||||
"-1.23 Wh"
|
||||
);
|
||||
});
|
||||
it("Conversion with target unit", () => {
|
||||
assert.strictEqual(
|
||||
formatConsumptionShort(hass, 0.00012, "kWh", "Wh"),
|
||||
"0.12 Wh"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatConsumptionShort(hass, 0.00012, "kWh", "kWh"),
|
||||
"0 kWh"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatConsumptionShort(hass, 0.01012, "kWh", "kWh"),
|
||||
"0.01 kWh"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatConsumptionShort(hass, 0.00012, "kWh", "MWh"),
|
||||
"0 MWh"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatConsumptionShort(hass, 10.12345, "kWh", "kWh"),
|
||||
"10.1 kWh"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatConsumptionShort(hass, 10.12345, "kWh", "ZZZZZWh"),
|
||||
"10.1 kWh"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatConsumptionShort(hass, 151234.5678, "kWh", "MWh"),
|
||||
"151 MWh"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Energy Usage Calculation Tests", () => {
|
||||
|
136
yarn.lock
136
yarn.lock
@@ -3818,22 +3818,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/client@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@rsdoctor/client@npm:1.1.7"
|
||||
checksum: 10/4d17a357414f50b8ecb52e22530b6e657106c70b9c94d41f90f2ed2c833fddede80b3d23fb1aba47584719fa704391413124010d3a48ff065a8666dd07028ec8
|
||||
"@rsdoctor/client@npm:1.1.8":
|
||||
version: 1.1.8
|
||||
resolution: "@rsdoctor/client@npm:1.1.8"
|
||||
checksum: 10/fe815e1d6f96a75dcc44d3da25ba1dbf30e07a448809f2c17c91b8d107278b62488f302735cde148c9d64ab4dd9920f0d2fa62c7511a82a328c29db20b903fbd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/core@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@rsdoctor/core@npm:1.1.7"
|
||||
"@rsdoctor/core@npm:1.1.8":
|
||||
version: 1.1.8
|
||||
resolution: "@rsdoctor/core@npm:1.1.8"
|
||||
dependencies:
|
||||
"@rsbuild/plugin-check-syntax": "npm:1.3.0"
|
||||
"@rsdoctor/graph": "npm:1.1.7"
|
||||
"@rsdoctor/sdk": "npm:1.1.7"
|
||||
"@rsdoctor/types": "npm:1.1.7"
|
||||
"@rsdoctor/utils": "npm:1.1.7"
|
||||
"@rsdoctor/graph": "npm:1.1.8"
|
||||
"@rsdoctor/sdk": "npm:1.1.8"
|
||||
"@rsdoctor/types": "npm:1.1.8"
|
||||
"@rsdoctor/utils": "npm:1.1.8"
|
||||
axios: "npm:^1.10.0"
|
||||
browserslist-load-config: "npm:^1.0.0"
|
||||
enhanced-resolve: "npm:5.12.0"
|
||||
@@ -3844,50 +3844,50 @@ __metadata:
|
||||
semver: "npm:^7.7.2"
|
||||
source-map: "npm:^0.7.4"
|
||||
webpack-bundle-analyzer: "npm:^4.10.2"
|
||||
checksum: 10/30a0adf465501cdaab1b8422529d21224935f61fb773a52075be4ba9d8ca684140bc1220e2ef1f77fbd75944645a564bd7eaba930dedb8c49f267fe0dcd99a73
|
||||
checksum: 10/1c71d9e2c25d8d7f52095c19be3c8784acb9ad9702103a5718d1c25bb20378af0490c3e95566f57c9aaf8e8c0c4b462dce75a5c1bbefb47021aff7365ae95b9c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/graph@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@rsdoctor/graph@npm:1.1.7"
|
||||
"@rsdoctor/graph@npm:1.1.8":
|
||||
version: 1.1.8
|
||||
resolution: "@rsdoctor/graph@npm:1.1.8"
|
||||
dependencies:
|
||||
"@rsdoctor/types": "npm:1.1.7"
|
||||
"@rsdoctor/utils": "npm:1.1.7"
|
||||
"@rsdoctor/types": "npm:1.1.8"
|
||||
"@rsdoctor/utils": "npm:1.1.8"
|
||||
lodash.unionby: "npm:^4.8.0"
|
||||
socket.io: "npm:4.8.1"
|
||||
source-map: "npm:^0.7.4"
|
||||
checksum: 10/4314beb5119c7082df8b046c23fa27e4bcd80e3a504188e8a9e0d84e58713fae32e720b2cc4639f04ef53e891a323871e9378d37548769961a87934eff79825d
|
||||
checksum: 10/26553f153ec865b20aa1e112a57147c2282580579885595fda1b5269c00389fdead5f31ba4afdda4c5a229d7ae5ab3b70a5859bbcaddaeb835a232cb3d3dae8f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/rspack-plugin@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@rsdoctor/rspack-plugin@npm:1.1.7"
|
||||
"@rsdoctor/rspack-plugin@npm:1.1.8":
|
||||
version: 1.1.8
|
||||
resolution: "@rsdoctor/rspack-plugin@npm:1.1.8"
|
||||
dependencies:
|
||||
"@rsdoctor/core": "npm:1.1.7"
|
||||
"@rsdoctor/graph": "npm:1.1.7"
|
||||
"@rsdoctor/sdk": "npm:1.1.7"
|
||||
"@rsdoctor/types": "npm:1.1.7"
|
||||
"@rsdoctor/utils": "npm:1.1.7"
|
||||
"@rsdoctor/core": "npm:1.1.8"
|
||||
"@rsdoctor/graph": "npm:1.1.8"
|
||||
"@rsdoctor/sdk": "npm:1.1.8"
|
||||
"@rsdoctor/types": "npm:1.1.8"
|
||||
"@rsdoctor/utils": "npm:1.1.8"
|
||||
lodash: "npm:^4.17.21"
|
||||
peerDependencies:
|
||||
"@rspack/core": "*"
|
||||
peerDependenciesMeta:
|
||||
"@rspack/core":
|
||||
optional: true
|
||||
checksum: 10/c2a4dfcf5bd18b59e1acadac62f8650847a634dfe469a5b15c217d94856f6a66170b743cc16af027f0c8096f05914423dac72fd2cfd453d95e1750918506d2b7
|
||||
checksum: 10/d01a41f19e812ba6eb90af57e8e376e70936fdac15c945d4e9787521a4acea03d808202a9afab6725df0c68a0521be8e7ffce50567a2be4d7a4d4c1bf1eecde0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/sdk@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@rsdoctor/sdk@npm:1.1.7"
|
||||
"@rsdoctor/sdk@npm:1.1.8":
|
||||
version: 1.1.8
|
||||
resolution: "@rsdoctor/sdk@npm:1.1.8"
|
||||
dependencies:
|
||||
"@rsdoctor/client": "npm:1.1.7"
|
||||
"@rsdoctor/graph": "npm:1.1.7"
|
||||
"@rsdoctor/types": "npm:1.1.7"
|
||||
"@rsdoctor/utils": "npm:1.1.7"
|
||||
"@rsdoctor/client": "npm:1.1.8"
|
||||
"@rsdoctor/graph": "npm:1.1.8"
|
||||
"@rsdoctor/types": "npm:1.1.8"
|
||||
"@rsdoctor/utils": "npm:1.1.8"
|
||||
"@types/fs-extra": "npm:^11.0.4"
|
||||
body-parser: "npm:1.20.3"
|
||||
cors: "npm:2.8.5"
|
||||
@@ -3895,18 +3895,18 @@ __metadata:
|
||||
fs-extra: "npm:^11.1.1"
|
||||
json-cycle: "npm:^1.5.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
open: "npm:^10.1.2"
|
||||
open: "npm:^8.4.2"
|
||||
sirv: "npm:2.0.4"
|
||||
socket.io: "npm:4.8.1"
|
||||
source-map: "npm:^0.7.4"
|
||||
tapable: "npm:2.2.2"
|
||||
checksum: 10/e2b24eb7ac5aaf2872a4f4d3483be65ca4e9d17311a1fc1b4648c05cb697b232a2be1be3a3f5dc2afbcfb2d92f4373fa2b5d80d926a49bb1cb7238a8e1e5c41f
|
||||
checksum: 10/b2b732a6bef8116b422b35dfdbd2805b556a169177d93e86ac20274bf160de8dd9be7bc50ad6aca0adddbac147353309166c4e2ba48e7e4741ecbd71a8b823ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/types@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@rsdoctor/types@npm:1.1.7"
|
||||
"@rsdoctor/types@npm:1.1.8":
|
||||
version: 1.1.8
|
||||
resolution: "@rsdoctor/types@npm:1.1.8"
|
||||
dependencies:
|
||||
"@types/connect": "npm:3.4.38"
|
||||
"@types/estree": "npm:1.0.5"
|
||||
@@ -3920,16 +3920,16 @@ __metadata:
|
||||
optional: true
|
||||
webpack:
|
||||
optional: true
|
||||
checksum: 10/fd5aec14068746fb25dda4e93c6a804b01970dfe0c5d4a7f30dc6097923dc19b105fd5e899b279c066cc027666fa814c401bbdb1a3988a31dea8686830856050
|
||||
checksum: 10/abecd025399cafeddb563789c88d259129974aa5092993db32bcee5674987a99f05a047c0a3b728fd23c7d8f0a850e303c12ba7404e13a8f3dc2fd5214495096
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/utils@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@rsdoctor/utils@npm:1.1.7"
|
||||
"@rsdoctor/utils@npm:1.1.8":
|
||||
version: 1.1.8
|
||||
resolution: "@rsdoctor/utils@npm:1.1.8"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:7.26.2"
|
||||
"@rsdoctor/types": "npm:1.1.7"
|
||||
"@rsdoctor/types": "npm:1.1.8"
|
||||
"@types/estree": "npm:1.0.5"
|
||||
acorn: "npm:^8.10.0"
|
||||
acorn-import-attributes: "npm:^1.9.5"
|
||||
@@ -3945,7 +3945,7 @@ __metadata:
|
||||
picocolors: "npm:^1.1.1"
|
||||
rslog: "npm:^1.2.8"
|
||||
strip-ansi: "npm:^6.0.1"
|
||||
checksum: 10/70b1fbf149f79d574c889f39e01303898ced2215d6a964d0359c7286e0609f6b9a057a5b54913b76a27312ca0469995bd05d2074fbc82879c59331cef1143ee2
|
||||
checksum: 10/74fc27f2878d044da0056c02d34bd6a893471fde240130cec01c0017ba8a85799c08b51203ae4ad0cd2481ac58ac029d49472d58433e7c7bc09bc2b298a9920c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6961,10 +6961,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"core-js@npm:3.43.0":
|
||||
version: 3.43.0
|
||||
resolution: "core-js@npm:3.43.0"
|
||||
checksum: 10/514952992863266b1a6a2d3c985e905461d37fe72d131d9320d5dbf01ac7e746f6fc53004b548347518cc832f7d2602b9a228acf6b5183e5cbede9dd296d73d3
|
||||
"core-js@npm:3.44.0":
|
||||
version: 3.44.0
|
||||
resolution: "core-js@npm:3.44.0"
|
||||
checksum: 10/759ef4ab0d12c9a6e8a32537d2b0fe64c2d7be40e13e0d7eb9604a970c380d64f37489dee03bd1c286c169e47a69d3ca2a968e8fcde0f78094ea22a20465d763
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7258,6 +7258,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"define-lazy-prop@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "define-lazy-prop@npm:2.0.0"
|
||||
checksum: 10/0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"define-lazy-prop@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "define-lazy-prop@npm:3.0.0"
|
||||
@@ -9367,7 +9374,7 @@ __metadata:
|
||||
"@octokit/plugin-retry": "npm:8.0.1"
|
||||
"@octokit/rest": "npm:22.0.0"
|
||||
"@replit/codemirror-indentation-markers": "npm:6.5.3"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.1.7"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.1.8"
|
||||
"@rspack/cli": "npm:1.4.4"
|
||||
"@rspack/core": "npm:1.4.4"
|
||||
"@shoelace-style/shoelace": "npm:2.20.1"
|
||||
@@ -9406,7 +9413,7 @@ __metadata:
|
||||
browserslist-useragent-regexp: "npm:4.1.3"
|
||||
color-name: "npm:2.0.0"
|
||||
comlink: "npm:4.4.2"
|
||||
core-js: "npm:3.43.0"
|
||||
core-js: "npm:3.44.0"
|
||||
cropperjs: "npm:1.6.2"
|
||||
date-fns: "npm:4.1.0"
|
||||
date-fns-tz: "npm:3.2.0"
|
||||
@@ -9479,7 +9486,7 @@ __metadata:
|
||||
typescript: "npm:5.8.3"
|
||||
typescript-eslint: "npm:8.35.1"
|
||||
ua-parser-js: "npm:2.0.4"
|
||||
vis-data: "npm:7.1.9"
|
||||
vis-data: "npm:7.1.10"
|
||||
vite-tsconfig-paths: "npm:5.1.4"
|
||||
vitest: "npm:3.2.4"
|
||||
vue: "npm:2.7.16"
|
||||
@@ -9992,7 +9999,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-docker@npm:^2.0.0":
|
||||
"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1":
|
||||
version: 2.2.1
|
||||
resolution: "is-docker@npm:2.2.1"
|
||||
bin:
|
||||
@@ -11834,7 +11841,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"open@npm:^10.0.3, open@npm:^10.1.2":
|
||||
"open@npm:^10.0.3":
|
||||
version: 10.1.2
|
||||
resolution: "open@npm:10.1.2"
|
||||
dependencies:
|
||||
@@ -11846,6 +11853,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"open@npm:^8.4.2":
|
||||
version: 8.4.2
|
||||
resolution: "open@npm:8.4.2"
|
||||
dependencies:
|
||||
define-lazy-prop: "npm:^2.0.0"
|
||||
is-docker: "npm:^2.1.1"
|
||||
is-wsl: "npm:^2.2.0"
|
||||
checksum: 10/acd81a1d19879c818acb3af2d2e8e9d81d17b5367561e623248133deb7dd3aefaed527531df2677d3e6aaf0199f84df57b6b2262babff8bf46ea0029aac536c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"opener@npm:^1.5.2":
|
||||
version: 1.5.2
|
||||
resolution: "opener@npm:1.5.2"
|
||||
@@ -14895,13 +14913,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vis-data@npm:7.1.9":
|
||||
version: 7.1.9
|
||||
resolution: "vis-data@npm:7.1.9"
|
||||
"vis-data@npm:7.1.10":
|
||||
version: 7.1.10
|
||||
resolution: "vis-data@npm:7.1.10"
|
||||
peerDependencies:
|
||||
uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
|
||||
uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||
vis-util: ^5.0.1
|
||||
checksum: 10/13cc6774cc225aa8a84d12c29d90188627fe8d937a93080113bd7b30d729fe8d6b2703322b41614ed48ad3f28f66e4090dfb1dce2ae7885a8938c2cffd7eebf3
|
||||
checksum: 10/23fb2ef26864153013372e1d95107765be86dd9ce96f987bf99fdd93759fbe5ec1bd2603d354ca18a03f0fb607b829396ec02fe005aead63ef24599512f21402
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user