Compare commits

...

35 Commits

Author SHA1 Message Date
Paul Bottein ebd8feb801 Rename entity name modes to inherit/specific 2026-06-24 14:16:16 +02:00
Paul Bottein 9e8d38ea63 Add entity type selector for main and additional entities 2026-06-18 17:02:21 +02:00
Paul Bottein 4fd976dc8c List main entities first on the device page (#52728)
* List main entities first on the device page

* Update src/panels/config/devices/device-detail/ha-device-entities-card.ts

Co-authored-by: Aidan Timson <aidan@timmo.dev>

---------

Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-06-18 14:38:37 +00:00
karwosts f8d8dc4eaa Fix entities timestamp editor (#52729) 2026-06-18 14:14:27 +01:00
Petar Petrov 8ccda740ee Fix date/datetime selectors on the design gallery and align datetime fields (#52726)
* Provide i18n and config contexts to gallery ha-selector demo

* Align date and time fields in datetime selector
2026-06-18 13:25:07 +02:00
Aidan Timson 8528dd8a15 Migrate more info person, sun, weather controls to lazy context (#52706) 2026-06-18 12:26:05 +03:00
Paulus Schoutsen ac2f8ebce3 Add radio frequency panel (#52464)
* Add rf panel

* Tweaks

* Align canShowPage with dev PageNavigation type

* Restore page filter check in canShowPage

* Add transmitters list to radio frequency panel

Show transmitter status and a devices data table (name, type, last used)
with links to the device info page, and use the radio tower domain icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update src/panels/config/integrations/integration-panels/radio_frequency/radio-frequency-transmitters.ts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* adjust comments

* Rename radio frequency transmitters page to devices page

Mirror the infrared panel: rename the transmitters page to a devices
page, source the type column label from the integration's
entity_component name, fetch transmitters in the router and pass them to
both pages, and align user-facing copy to "devices".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Handle radio frequency transmitter load errors

Wrap the transmitter fetch in try/catch and surface failures via an
alert dialog instead of leaving an unhandled rejection.

Also drop the duplicate PageNavigation.filter declaration introduced by
the dev merge (it already exists on dev).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-06-18 09:25:38 +00:00
renovate[bot] 1462f65f5a Update yarn monorepo to v4.17.0 (#52725)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 12:22:58 +03:00
renovate[bot] 3e9d3d90a1 Update vitest monorepo to v4.1.9 (#52724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 09:21:56 +00:00
Aidan Timson f28898551b Reword advanced controls to more controls in siren (#52700)
Reword advanced to more in siren
2026-06-18 08:25:34 +03:00
Aidan Timson eabbcf3a95 Migrate more info lock / alarm to lazy context (#52703)
Migrate more info lock / alarm
2026-06-18 08:24:44 +03:00
Aidan Timson 01255cebc6 Remove "advanced" from security options in zwave search (#52702) 2026-06-18 08:24:17 +03:00
Aidan Timson d20e062de9 Migrate more info vaccum / lawn mower controls to lazy context (#52704) 2026-06-18 08:23:28 +03:00
Aidan Timson 835c0fa35c Migrate more info fan and light controls to lazy context (#52705) 2026-06-18 08:22:48 +03:00
Aidan Timson e308272d89 Migrate more info media controls to lazy context (#52707)
* Migrate more info media controls to lazy context

* Remove
2026-06-18 08:21:36 +03:00
renovate[bot] d2ae376058 Update Node.js to v24.17.0 (#52721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-18 05:21:21 +00:00
Paulus Schoutsen fdd57645ee Add infrared panel (#52465)
* Add IR panel

* Tweaks

* Redesign infrared panel: device dashboard + table

Show a Bluetooth-style status card with the count of online IR devices,
linking to a separate data-table page that lists devices grouped from
their proxy entities. Devices expose a type (Emitter, Receiver, or
"Receiver, Emitter") and a "Last used" column derived from the most
recent entity state timestamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Pass localize instead of hass into _data memoizeOne

* Fix Prettier formatting in infrared-devices-page.ts

* Derive infrared devices from registries instead of infrared/list

Drop the infrared/list WebSocket call and compute the device dataset
from the entity/device registries in the dashboard router, passing it
down to the dashboard and devices pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Use infrared entity_component device class names for type labels

* Remove fallback strings from localize calls in infrared devices page

* Fix device class translation

* Cleanup

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 07:20:16 +02:00
karwosts 1ef1655a4c Add time_format selector to entities card entity-row-editor (#52715) 2026-06-18 08:18:31 +03:00
Paul Bottein aa1108fc41 Translate list attributes and device class in entity details (#52716) 2026-06-18 08:17:34 +03:00
karwosts e3c6a57080 Add short / long timestamp styles (#52719) 2026-06-18 08:16:34 +03:00
karwosts 1e22649ef8 Unify timestamp state domain lists (#52717) 2026-06-18 07:11:45 +02:00
karwosts 5abd04d09a Convert remaining EntityFeatures to enums (#52720) 2026-06-18 07:10:00 +02:00
karwosts a5bf35690b Add time format to entity badge (#52713) 2026-06-17 20:24:03 +02:00
karwosts d98eb47490 Decode supported features in more-info-details (#52712)
* Decode supported features in more-info-details

* Remove 'Supported features' translation entry
2026-06-17 18:33:18 +02:00
karwosts 738e92d27d Add time_format to tile card (#52450)
* Add time_format to tile card

* Updates

* incorrect type

* Apply suggestions from code review

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* code review feedback

* handle timestamp=0

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-06-17 16:04:40 +02:00
Aidan Timson ade2e9272b Reword advanced settings to more options in helpers (#52701) 2026-06-17 15:47:24 +02:00
Simon Lamon d8ce60dfb6 Add icons to live condition test (#52458)
Add icons to live condition
2026-06-17 16:41:15 +03:00
Aidan Timson db9374925e Migrate more info climate (+ related) to lazy context (#52694)
* Migrate more info climate (+ related) to lazy context

* Remove hass
2026-06-17 13:19:46 +00:00
Aidan Timson 1bcd1293c0 Reword "advanced concept" in event trigger/action descriptions (#52699) 2026-06-17 16:07:57 +03:00
Aidan Timson b8cf061ebb Migrate more info datetime (+ related) to lazy context (#52696) 2026-06-17 16:01:01 +03:00
karwosts 6585da9a73 Fix continue_on_timeout toggle defaults in wait script actions (#52691)
Fix continue_on_timeout toggle in wait script actions
2026-06-17 13:26:21 +02:00
Paul Bottein 368df82e97 Redesign the Activity (logbook) as a timeline with entity context (#52498)
* Redesign the Activity (logbook) as a timeline with entity context

* Update color

* Refine logbook timeline layout and entry rendering

- Three layout modes in ha-logbook-entry: wide (entity → state inline),
  compact (entity/state + context/time), inline (state + cause icon + time)
- Entity name bold in wide and compact modes, consistent with tile card
- Cause icon shown inline next to the time in inline (single-entity) mode
- Unavailable state rendered as an empty circle dot
- Flash icon for entity-triggered causes
- "Show more" chevron link in logbook card, device page, and area page
- Extract _renderWide / _renderCompact / _renderInline from render()
- Scope entity-name flex layout to .line1 > .entity-name (compact only)

Co-Authored-By: Paul Bottein <paul.bottein@gmail.com>

* Show cause icon in inline logbook entries

- Show cause icon (user avatar, trigger type, integration brand) next to
  the time in single-entity inline mode
- Use ha-trigger-icon for trigger-platform causes
- Use ha-domain-icon with brand-fallback for integration causes when
  context_domain is available, falling back to mdiPuzzle
- Tooltip with cause name on hover
- Icon size 18px, user avatar 18x18px

Co-Authored-By: Paul Bottein <paul.bottein@gmail.com>

* Adjust cause icon sizes: 18px standalone, 16px inline with text

Co-Authored-By: Paul Bottein <paul.bottein@gmail.com>

* Fix somes issues

* Refine logbook timeline rendering

* Fix logbook dot alignment, header link, and graph colors

* Use deterministic colors for select/input_select in logbook timeline

Assign colors by options list index instead of encounter order so
logbook dots always match the history chart colors, regardless of JS
chunk boundaries.

* Add relative time to logbook entries

Show short relative time alongside absolute in all layouts.
Cause moves to its own third line in compact when icon mode is active.

* Replace dual time display with click-to-toggle in logbook

Clicking any time value toggles between absolute (default) and relative
short format. State lives in the renderer and propagates via Lit
re-render when shouldUpdate allows it.

Date headers now show "Today · June 15" and "Yesterday · June 14"
for recent dates via Intl.RelativeTimeFormat.

* Fix time toggle not updating entries in virtualizer mode

Use @queryAll to directly update showRelative on all visible entries
after toggling, covering the virtualizer case where Lit re-render
alone does not propagate prop changes to already-rendered items.

Also remove the !item guard in _renderRow to fix the RenderItemFunction<T>
type mismatch.

* Refine logbook compact/wide layout and cause display

- Move time column to the right in wide layout
- Right-align time in compact cause row by wrapping cause+trace in meta-main
- Hide cause icon/label for automation and script entries in compact/inline mode (only show trace link)
- Make automation/script entity name always clickable (opens more-info)

* Refactor logbook cause into typed kinds with text phrases

Replace the untyped `iconPath`/`triggerPlatform` fields on `LogbookCause`
with a `kind` discriminator (`user`, `automation`, `script`, `state`,
`scheduled`, `homeassistant`, `integration`).

In timeline layout, causes now render as readable text phrases
("By Paul", "By automation: Mode nuit", "By state change: Porte entrée",
"Scheduled", "Via HomeKit") with a `·` separator before "View trace".
Entity names in those phrases are clickable when an entity id is available.

In list/inline layout, the icon badge uses the kind to pick the right
icon (avatar, robot, script, brand domain, puzzle) — no trigger-type
icon component needed anymore.

* Add show-cause mode to logbook list layout

Add a `show-cause` boolean prop to `ha-logbook-entry`, `ha-logbook-renderer`,
and `ha-logbook` that switches list mode from a compact icon badge to a full
cause phrase on a third line.

The third line uses a fixed-width prefix span and a flex-1 truncatable entity
button so long automation/script/entity names ellipsize cleanly. The trace
link always stays right-aligned on the same line.

Enable the mode in `ha-panel-logbook` so the main activity feed shows full
cause context for every entry.

* Rename logbook model identifiers to match HA conventions and clean up

- Rename resolve*/build* → compute*, kind → type, LogbookWhat → LogbookValue,
  model.what → model.value across model, renderer, and tests
- Merge EntryRenderCtx into LogbookRenderItem (extends LogbookItem) so layout
  methods receive one flat object instead of ctx.model.xxx
- Inline _causeUser, drop dead possibleEntity branch in message formatter
- Remove unused .cause and .cause-name CSS classes; fix padding-block
  inconsistency on timeline content

* Use ha-relative-time in logbook for auto-updating relative times

Replace the static relativeTime() string with <ha-relative-time> so the
displayed time updates every 60 s without a full re-render. Add a format
prop (Intl.RelativeTimeFormatStyle) to ha-relative-time to support the
short style needed by the logbook. Fix text-overflow ellipsis in the time
column by restructuring .time to use align-items: stretch with an inner
.time-content block that owns overflow/ellipsis, and display: contents on
ha-relative-time so its text participates in the parent's inline flow.

Also rename computeLogbookItem's internal param from item to entry to
avoid shadowing the outer item variable.

* Fix automation run value detection and timeline arrow display

User-triggered automation runs had context_user_id set but no source or
context_event_type, so isAutomationRun was false and the raw backend
message "triggered" (lowercase) was shown instead of the localized
"Triggered". Add context_user_id to the isAutomationRun check so all
automation runs get the proper localized value.

Restore the state arrow (→) in the timeline for all value.type === "state"
entries, including automation runs.

* Fix ha-relative-time interval and use textContent

The 60-second auto-update interval was never started when datetime is set
via Lit property binding, because connectedCallback runs before Lit sets
properties. Move the interval start/stop logic into update() watching the
datetime property change instead.

Also replace innerHTML with textContent since the relative time string is
always plain text.

* Remove comments

* Feedback
2026-06-17 13:23:08 +02:00
Aidan Timson 1d99a5dff9 Migrate more info actions to lazy context (#52693)
* Migrate more info actions to lazy context

* Restore file while hass is still needed down the deep chain
2026-06-17 12:23:52 +03:00
Aidan Timson 0ca72b763a Migrate more info toggles to lazy context (#52692) 2026-06-17 08:33:38 +00:00
Aidan Timson 31848a1efd Migrate more info cover + valve to lazy context (#52695) 2026-06-17 11:18:43 +03:00
189 changed files with 6319 additions and 2635 deletions
+1 -1
View File
@@ -1 +1 @@
24.16.0
24.17.0
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,4 +13,4 @@ nodeLinker: node-modules
npmMinimalAgeGate: 3d
yarnPath: .yarn/releases/yarn-4.16.0.cjs
yarnPath: .yarn/releases/yarn-4.17.0.cjs
+28 -1
View File
@@ -1,4 +1,5 @@
import type { TemplateResult } from "lit";
import { ContextProvider } from "@lit/context";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
@@ -14,6 +15,11 @@ import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint";
import {
configContext,
internationalizationContext,
} from "../../../../src/data/context";
import { updateHassGroups } from "../../../../src/data/context/updateContext";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
@@ -518,6 +524,17 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
private data = SCHEMAS.map(() => ({}));
// The date/datetime selectors and the date-picker dialog consume these
// contexts (provided by the root element in the real app). Provide them here
// so they work in the gallery.
private _i18nProvider = new ContextProvider(this, {
context: internationalizationContext,
});
private _configProvider = new ContextProvider(this, {
context: configContext,
});
constructor() {
super();
const hass = provideHass(this);
@@ -539,6 +556,16 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
el.hass = this.hass;
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("hass") && this.hass) {
this._i18nProvider.setValue(
updateHassGroups.internationalization(this.hass)
);
this._configProvider.setValue(updateHassGroups.config(this.hass));
}
}
public connectedCallback() {
super.connectedCallback();
this.addEventListener("show-dialog", this._dialogManager);
+4 -4
View File
@@ -154,7 +154,7 @@
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@vitest/coverage-v8": "4.1.8",
"@vitest/coverage-v8": "4.1.9",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.4",
@@ -197,7 +197,7 @@
"typescript": "6.0.3",
"typescript-eslint": "8.61.0",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.8",
"vitest": "4.1.9",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
@@ -213,8 +213,8 @@
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
},
"packageManager": "yarn@4.16.0",
"packageManager": "yarn@4.17.0",
"volta": {
"node": "24.16.0"
"node": "24.17.0"
}
}
+2 -1
View File
@@ -4,7 +4,8 @@ import { ensureArray } from "../array/ensure-array";
import { isComponentLoaded } from "./is_component_loaded";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
isCore(page) || isLoadedIntegration(hass, page);
(isCore(page) || isLoadedIntegration(hass, page)) &&
(!page.filter || page.filter(hass));
export const isLoadedIntegration = (
hass: HomeAssistant,
+19
View File
@@ -110,6 +110,25 @@ export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
"media_player",
]);
/** Domains that use a timestamp for state. */
export const TIMESTAMP_STATE_DOMAINS = new Set([
"ai_task",
"button",
"conversation",
"event",
"image",
"infrared",
"input_button",
"notify",
"radio_frequency",
"scene",
"stt",
"tag",
"tts",
"wake_word",
"datetime",
]);
/** Temperature units. */
export const UNIT_C = "°C";
export const UNIT_F = "°F";
+6 -5
View File
@@ -3,23 +3,24 @@ import type { FrontendLocaleData } from "../../data/translation";
import { selectUnit } from "../util/select-unit";
const formatRelTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" })
(locale: FrontendLocaleData, style: Intl.RelativeTimeFormatStyle) =>
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto", style })
);
export const relativeTime = (
from: Date,
locale: FrontendLocaleData,
to?: Date,
includeTense = true
includeTense = true,
style: Intl.RelativeTimeFormatStyle = "long"
): string => {
const diff = selectUnit(from, to, locale);
if (includeTense) {
return formatRelTimeMem(locale).format(diff.value, diff.unit);
return formatRelTimeMem(locale, style).format(diff.value, diff.unit);
}
return Intl.NumberFormat(locale.language, {
style: "unit",
unit: diff.unit,
unitDisplay: "long",
unitDisplay: style,
}).format(Math.abs(diff.value));
};
@@ -60,6 +60,17 @@ export const computeAttributeValueToParts = (
return [{ type: "value", value: localize("state.default.unknown") }];
}
// Device class attribute, return the integration's translated name
if (attribute === "device_class" && typeof attributeValue === "string") {
const domain = computeStateDomain(stateObj);
const deviceClassName = localize(
`component.${domain}.entity_component.${attributeValue}.name`
);
if (deviceClassName) {
return [{ type: "value", value: deviceClassName }];
}
}
// Number value, return formatted number
if (typeof attributeValue === "number") {
const domain = computeStateDomain(stateObj);
+3 -2
View File
@@ -30,7 +30,7 @@ export const computeEntityEntryName = (
fallbackStateObj?: HassEntity
): string | undefined => {
const name =
entry.name ||
entry.name ??
("original_name" in entry && entry.original_name != null
? String(entry.original_name)
: undefined);
@@ -59,7 +59,8 @@ export const computeEntityEntryName = (
return stripPrefixFromEntityName(name, deviceName) || name;
}
return name;
// Empty name = main entity → undefined, so callers fall back to the device name.
return name || undefined;
};
export const entityUseDeviceName = (
+2 -20
View File
@@ -21,29 +21,11 @@ import {
isNumericSensorDeviceClass,
SENSOR_TIMESTAMP_DEVICE_CLASSES,
} from "../../data/sensor";
import { TIMESTAMP_STATE_DOMAINS } from "../const";
// Domains whose state is a timezone-agnostic date and/or time string.
const DATE_TIME_DOMAINS = new Set(["date", "input_datetime", "time"]);
// Domains whose state is a timestamp.
const TIMESTAMP_DOMAINS = new Set([
"ai_task",
"button",
"conversation",
"event",
"image",
"infrared",
"input_button",
"notify",
"radio_frequency",
"scene",
"stt",
"tag",
"tts",
"wake_word",
"datetime",
]);
// Maps Intl.NumberFormat part types to ValuePart types for monetary states.
const MONETARY_TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
@@ -273,7 +255,7 @@ const computeStateToPartsFromEntityAttributes = (
// state is a timestamp
if (
TIMESTAMP_DOMAINS.has(domain) ||
TIMESTAMP_STATE_DOMAINS.has(domain) ||
(domain === "sensor" &&
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(attributes.device_class))
) {
-23
View File
@@ -1,23 +0,0 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { supportsFeature } from "./supports-feature";
export type FeatureClassNames<T extends number = number> = Partial<
Record<T, string>
>;
// Expects classNames to be an object mapping feature-bit -> className
export const featureClassNames = (
stateObj: HassEntity,
classNames: FeatureClassNames
) => {
if (!stateObj || !stateObj.attributes.supported_features) {
return "";
}
return Object.keys(classNames)
.map((feature) =>
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
)
.filter((attr) => attr !== "")
.join(" ");
};
+56
View File
@@ -0,0 +1,56 @@
import { AITaskEntityFeature } from "../../data/ai_task";
import { AlarmControlPanelEntityFeature } from "../../data/alarm_control_panel";
import { AssistSatelliteEntityFeature } from "../../data/assist_satellite";
import { CalendarEntityFeature } from "../../data/calendar";
import { CameraEntityFeature } from "../../data/camera";
import { ClimateEntityFeature } from "../../data/climate";
import { ConversationEntityFeature } from "../../data/conversation";
import { CoverEntityFeature } from "../../data/cover";
import { FanEntityFeature } from "../../data/fan";
import { HumidifierEntityFeature } from "../../data/humidifier";
import { LawnMowerEntityFeature } from "../../data/lawn_mower";
import { LightEntityFeature } from "../../data/light";
import { LockEntityFeature } from "../../data/lock";
import { MediaPlayerEntityFeature } from "../../data/media-player";
import { NotifyEntityFeature } from "../../data/notify";
import { RemoteEntityFeature } from "../../data/remote";
import { SirenEntityFeature } from "../../data/siren";
import { TodoListEntityFeature } from "../../data/todo";
import { UpdateEntityFeature } from "../../data/update";
import { VacuumEntityFeature } from "../../data/vacuum";
import { ValveEntityFeature } from "../../data/valve";
import { WaterHeaterEntityFeature } from "../../data/water_heater";
import { WeatherEntityFeature } from "../../data/weather";
export type FeatureEnum = Record<string | number, string | number>;
const DOMAIN_ENUMS = {
ai_task: AITaskEntityFeature,
alarm_control_panel: AlarmControlPanelEntityFeature,
assist_satellite: AssistSatelliteEntityFeature,
calendar: CalendarEntityFeature,
camera: CameraEntityFeature,
climate: ClimateEntityFeature,
conversation: ConversationEntityFeature,
cover: CoverEntityFeature,
fan: FanEntityFeature,
humidifier: HumidifierEntityFeature,
lawn_mower: LawnMowerEntityFeature,
light: LightEntityFeature,
lock: LockEntityFeature,
media_player: MediaPlayerEntityFeature,
notify: NotifyEntityFeature,
remote: RemoteEntityFeature,
siren: SirenEntityFeature,
todo: TodoListEntityFeature,
update: UpdateEntityFeature,
vacuum: VacuumEntityFeature,
valve: ValveEntityFeature,
water_heater: WaterHeaterEntityFeature,
weather: WeatherEntityFeature,
};
export function getFeatures(domain: string): FeatureEnum | undefined {
const enumObj = DOMAIN_ENUMS[domain] as FeatureEnum;
return enumObj;
}
+84 -76
View File
@@ -22,16 +22,13 @@ export const FIXED_DOMAIN_STATES = {
assist_satellite: ["idle", "listening", "responding", "processing"],
automation: ["on", "off"],
binary_sensor: ["on", "off"],
button: [],
calendar: ["on", "off"],
camera: ["idle", "recording", "streaming"],
cover: ["closed", "closing", "open", "opening"],
device_tracker: ["home", "not_home"],
fan: ["on", "off"],
humidifier: ["on", "off"],
infrared: [],
input_boolean: ["on", "off"],
input_button: [],
lawn_mower: ["error", "paused", "mowing", "returning", "docked"],
light: ["on", "off"],
lock: [
@@ -56,7 +53,6 @@ export const FIXED_DOMAIN_STATES = {
plant: ["ok", "problem"],
radio_frequency: [],
remote: ["on", "off"],
scene: [],
schedule: ["on", "off"],
script: ["on", "off"],
siren: ["on", "off"],
@@ -290,6 +286,81 @@ export const getStatesDomain = (
return result;
};
// Maps a value attribute (or the main state, keyed `_`) to the attribute listing
// its options. Naming is irregular per domain, so it's mapped explicitly.
export const DOMAIN_OPTIONS_ATTRIBUTES: Record<
string,
Record<string, string>
> = {
climate: {
_: "hvac_modes",
fan_mode: "fan_modes",
preset_mode: "preset_modes",
swing_mode: "swing_modes",
swing_horizontal_mode: "swing_horizontal_modes",
},
event: {
event_type: "event_types",
},
fan: {
preset_mode: "preset_modes",
},
humidifier: {
mode: "available_modes",
},
input_select: {
_: "options",
},
select: {
_: "options",
},
light: {
effect: "effect_list",
color_mode: "supported_color_modes",
},
media_player: {
sound_mode: "sound_mode_list",
source: "source_list",
},
remote: {
current_activity: "activity_list",
},
sensor: {
_: "options",
},
vacuum: {
fan_speed: "fan_speed_list",
},
water_heater: {
_: "operation_list",
operation_mode: "operation_list",
},
};
const DOMAIN_VALUE_ATTRIBUTES: Record<
string,
Record<string, string>
> = Object.fromEntries(
Object.entries(DOMAIN_OPTIONS_ATTRIBUTES).map(([domain, mapping]) => [
domain,
Object.fromEntries(
Object.entries(mapping).map(([value, list]) => [list, value])
),
])
);
// value attribute (or main state) → its options-list attribute
export const getOptionsAttribute = (
domain: string,
attribute?: string
): string | undefined => DOMAIN_OPTIONS_ATTRIBUTES[domain]?.[attribute ?? "_"];
// options-list attribute → its value attribute (`_` = main state)
export const getValueAttribute = (
domain: string,
optionsAttribute: string
): string | undefined => DOMAIN_VALUE_ATTRIBUTES[domain]?.[optionsAttribute];
export const getStates = (
hass: HomeAssistant,
state: HassEntity,
@@ -302,78 +373,15 @@ export const getStates = (
result.push(...getStatesDomain(hass, domain, attribute));
// Dynamic values based on the entities
switch (domain) {
case "climate":
if (!attribute) {
result.push(...state.attributes.hvac_modes);
} else if (attribute === "fan_mode") {
result.push(...state.attributes.fan_modes);
} else if (attribute === "preset_mode") {
result.push(...state.attributes.preset_modes);
} else if (attribute === "swing_mode") {
result.push(...state.attributes.swing_modes);
} else if (attribute === "swing_horizontal_mode") {
result.push(...state.attributes.swing_horizontal_modes);
}
break;
case "event":
if (attribute === "event_type") {
result.push(...state.attributes.event_types);
}
break;
case "fan":
if (attribute === "preset_mode") {
result.push(...state.attributes.preset_modes);
}
break;
case "humidifier":
if (attribute === "mode") {
result.push(...state.attributes.available_modes);
}
break;
case "input_select":
case "select":
if (!attribute) {
result.push(...state.attributes.options);
}
break;
case "light":
if (attribute === "effect" && state.attributes.effect_list) {
result.push(...state.attributes.effect_list);
} else if (
attribute === "color_mode" &&
state.attributes.supported_color_modes
) {
result.push(...state.attributes.supported_color_modes);
}
break;
case "media_player":
if (attribute === "sound_mode") {
result.push(...state.attributes.sound_mode_list);
} else if (attribute === "source") {
result.push(...state.attributes.source_list);
}
break;
case "remote":
if (attribute === "current_activity") {
result.push(...state.attributes.activity_list);
}
break;
case "sensor":
if (!attribute && state.attributes.device_class === "enum") {
result.push(...state.attributes.options);
}
break;
case "vacuum":
if (attribute === "fan_speed") {
result.push(...state.attributes.fan_speed_list);
}
break;
case "water_heater":
if (!attribute || attribute === "operation_mode") {
result.push(...state.attributes.operation_list);
}
break;
const optionsAttribute = getOptionsAttribute(domain, attribute);
if (optionsAttribute) {
const options = state.attributes[optionsAttribute];
// Sensors only expose their options when their device class is `enum`.
const enumSensor =
domain !== "sensor" || state.attributes.device_class === "enum";
if (enumSensor && Array.isArray(options)) {
result.push(...options);
}
}
return [...new Set(result)];
+2 -10
View File
@@ -1,21 +1,13 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { OFF, UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { computeDomain } from "./compute_domain";
import { TIMESTAMP_STATE_DOMAINS } from "../const";
export function stateActive(stateObj: HassEntity, state?: string): boolean {
const domain = computeDomain(stateObj.entity_id);
const compareState = state !== undefined ? state : stateObj?.state;
if (
[
"button",
"event",
"infrared",
"input_button",
"radio_frequency",
"scene",
].includes(domain)
) {
if (TIMESTAMP_STATE_DOMAINS.has(domain)) {
return compareState !== UNAVAILABLE;
}
-2
View File
@@ -17,8 +17,6 @@ export type LocalizeKeys =
| `ui.common.${string}`
| `ui.components.calendar.event.rrule.${string}`
| `ui.components.selectors.file.${string}`
| `ui.components.logbook.messages.detected_device_classes.${string}`
| `ui.components.logbook.messages.cleared_device_classes.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.lawn_mower.${string}`
| `ui.dialogs.more_info_control.vacuum.${string}`
@@ -4,10 +4,10 @@ type ResultCache<T> = Record<string, Promise<T> | undefined>;
/**
* Call a function with result caching per entity.
* @param cacheKey key to store the cache on hass object
* @param cacheKey key to namespace the cache
* @param cacheTime time to cache the results
* @param func function to fetch the data
* @param hass Home Assistant object
* @param hass Home Assistant object (or slice) the cache is keyed on
* @param entityId entity to fetch data for
* @param args extra arguments to pass to the function to fetch the data
* @returns
@@ -15,8 +15,12 @@ type ResultCache<T> = Record<string, Promise<T> | undefined>;
export const timeCacheEntityPromiseFunc = async <T>(
cacheKey: string,
cacheTime: number,
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
hass: HomeAssistant,
func: (
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
entityId: string,
...args: any[]
) => Promise<T>,
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
entityId: string,
...args: any[]
): Promise<T> => {
@@ -39,11 +43,11 @@ export const timeCacheEntityPromiseFunc = async <T>(
// When successful, set timer to clear cache
() =>
setTimeout(() => {
cache![entityId] = undefined;
cache[entityId] = undefined;
}, cacheTime),
// On failure, clear cache right away
() => {
cache![entityId] = undefined;
cache[entityId] = undefined;
}
);
@@ -1,5 +1,12 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-svg-icon";
import {
mdiAlertCircle,
mdiCheckCircle,
mdiCloseCircle,
mdiHelpCircle,
} from "@mdi/js";
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
@@ -19,46 +26,59 @@ export class HaAutomationRowLiveTest extends LitElement {
@property() public label = "";
private get _iconPath() {
switch (this.state) {
case "pass":
return mdiCheckCircle;
case "fail":
return mdiCloseCircle;
case "invalid":
return mdiAlertCircle;
default:
return mdiHelpCircle;
}
}
protected render() {
return html`
<div
id="indicator"
role="status"
tabindex="0"
aria-label=${this.label}
></div>
<div id="indicator" role="status" tabindex="0" aria-label=${this.label}>
<ha-svg-icon .path=${this._iconPath}></ha-svg-icon>
</div>
`;
}
static styles = css`
:host {
position: absolute;
top: -5px;
inset-inline-end: -6px;
top: -8px;
inset-inline-end: -8px;
display: inline-block;
}
#indicator {
width: 10px;
height: 10px;
width: 16px;
height: 16px;
display: grid;
place-items: center;
border-radius: var(--ha-border-radius-circle);
border: var(--ha-border-width-md) solid;
box-sizing: border-box;
background-color: var(--card-background-color);
box-shadow: 0 0 0 2px var(--card-background-color);
transition: all var(--ha-animation-duration-normal) ease-in-out;
}
#indicator ha-svg-icon {
width: 16px;
height: 16px;
--mdc-icon-size: 16px;
}
:host([state="pass"]) #indicator {
background-color: var(--ha-color-green-60);
border-color: var(--ha-color-green-60);
color: var(--ha-color-green-60);
}
:host([state="fail"]) #indicator {
border-color: var(--ha-color-orange-60);
color: var(--ha-color-orange-60);
}
:host([state="invalid"]) #indicator {
border-color: var(--ha-color-red-60);
color: var(--ha-color-red-60);
}
:host([state="unknown"]) #indicator {
border-color: var(--ha-color-neutral-60);
color: var(--ha-color-neutral-60);
}
`;
}
+5 -3
View File
@@ -79,9 +79,11 @@ function computeTimelineEnumColor(
const domain = computeStateDomain(stateObj);
const states =
FIXED_DOMAIN_STATES[domain] ||
(domain === "sensor" &&
stateObj.attributes.device_class === "enum" &&
stateObj.attributes.options) ||
((domain === "sensor" && stateObj.attributes.device_class === "enum") ||
domain === "select" ||
domain === "input_select"
? stateObj.attributes.options
: undefined) ||
[];
const idx = states.indexOf(state);
if (idx === -1) {
+46 -20
View File
@@ -1,3 +1,4 @@
import { consume } from "@lit/context";
import {
mdiAlertCircle,
mdiChevronDown,
@@ -10,7 +11,9 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { supportsFeature } from "../common/entity/supports-feature";
import type { LocalizeFunc } from "../common/translations/localize";
import {
runAssistPipeline,
type AssistPipeline,
@@ -18,10 +21,19 @@ import {
type ConversationChatLogToolResultDelta,
type PipelineRunEvent,
} from "../data/assist_pipeline";
import {
configContext,
connectionContext,
statesContext,
} from "../data/context";
import { ConversationEntityFeature } from "../data/conversation";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import type {
HomeAssistant,
HomeAssistantConfig,
HomeAssistantConnection,
} from "../types";
import { AudioRecorder } from "../util/audio-recorder";
import { documentationUrl } from "../util/documentation-url";
import "./ha-alert";
@@ -47,8 +59,6 @@ interface AssistMessage {
@customElement("ha-assist-chat")
export class HaAssistChat extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public pipeline?: AssistPipeline;
@property({ type: Boolean, attribute: "disable-speech" })
@@ -71,6 +81,22 @@ export class HaAssistChat extends LitElement {
@state() private _processing = false;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HomeAssistant["states"];
@state()
@consume({ context: configContext, subscribe: true })
private _config!: HomeAssistantConfig;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: HomeAssistantConnection;
private _conversationId: string | null = null;
private _audioRecorder?: AudioRecorder;
@@ -86,7 +112,7 @@ export class HaAssistChat extends LitElement {
this._conversation = [
{
who: "hass",
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
text: this._localize("ui.dialogs.voice_command.how_can_i_help"),
thinking: "",
tool_calls: {},
},
@@ -124,9 +150,9 @@ export class HaAssistChat extends LitElement {
const controlHA = !this.pipeline
? false
: this.pipeline.prefer_local_intents ||
(this.hass.states[this.pipeline.conversation_engine]
(this._states[this.pipeline.conversation_engine]
? supportsFeature(
this.hass.states[this.pipeline.conversation_engine],
this._states[this.pipeline.conversation_engine],
ConversationEntityFeature.CONTROL
)
: true);
@@ -139,7 +165,7 @@ export class HaAssistChat extends LitElement {
? nothing
: html`
<ha-alert>
${this.hass.localize(
${this._localize(
"ui.dialogs.voice_command.conversation_no_control"
)}
</ha-alert>
@@ -180,7 +206,7 @@ export class HaAssistChat extends LitElement {
.path=${mdiCommentProcessingOutline}
></ha-svg-icon>
<span class="thinking-label">
${this.hass.localize(
${this._localize(
"ui.dialogs.voice_command.show_details"
)}
</span>
@@ -251,7 +277,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
id="message-input"
@keyup=${this._handleKeyUp}
@input=${this._handleInput}
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
.label=${this._localize(`ui.dialogs.voice_command.input_label`)}
>
<div slot="end">
${this._showSendButton || !supportsSTT
@@ -261,7 +287,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
.path=${mdiSend}
@click=${this._handleSendMessage}
.disabled=${this._processing}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.voice_command.send_text"
)}
>
@@ -282,7 +308,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
.path=${mdiMicrophone}
@click=${this._handleListeningButton}
.disabled=${this._processing}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.voice_command.start_listening"
)}
>
@@ -391,21 +417,21 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
text:
// New lines matter for messages
// prettier-ignore
html`${this.hass.localize(
html`${this._localize(
"ui.dialogs.voice_command.not_supported_microphone_browser"
)}
${this.hass.localize(
${this._localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation",
{
documentation_link: html`<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(
this.hass,
this._config,
"/docs/configuration/securing/#remote-access"
)}
>${this.hass.localize(
>${this._localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
)}</a>`,
}
@@ -443,7 +469,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
try {
const unsub = await runAssistPipeline(
this.hass,
this._connection,
(event: PipelineRunEvent) => {
if (event.type === "run-start") {
this._stt_binary_handler_id =
@@ -539,7 +565,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
}
private _sendAudioChunk(chunk: Int16Array) {
this.hass.connection.socket!.binaryType = "arraybuffer";
this._connection.connection.socket!.binaryType = "arraybuffer";
// eslint-disable-next-line eqeqeq
if (this._stt_binary_handler_id == undefined) {
@@ -550,7 +576,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
data[0] = this._stt_binary_handler_id;
data.set(new Uint8Array(chunk.buffer), 1);
this.hass.connection.socket!.send(data);
this._connection.connection.socket!.send(data);
}
private _unloadAudio = () => {
@@ -570,7 +596,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
hassMessageProcesser.addMessage();
try {
const unsub = await runAssistPipeline(
this.hass,
this._connection,
(event) => {
if (event.type.startsWith("intent-")) {
hassMessageProcesser.processEvent(event);
@@ -593,7 +619,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
);
} catch {
hassMessageProcesser.setError(
this.hass.localize("ui.dialogs.voice_command.error")
this._localize("ui.dialogs.voice_command.error")
);
} finally {
this._processing = false;
+24 -6
View File
@@ -1,16 +1,20 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import {
configContext,
connectionContext,
entitiesContext,
} from "../data/context";
import { attributeIcon } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-attribute-icon")
export class HaAttributeIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property() public attribute?: string;
@@ -19,6 +23,18 @@ export class HaAttributeIcon extends LitElement {
@property() public icon?: string;
@state()
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities?: ContextType<typeof entitiesContext>;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -28,12 +44,14 @@ export class HaAttributeIcon extends LitElement {
return nothing;
}
if (!this.hass) {
if (!this._config || !this._connection || !this._entities) {
return nothing;
}
const icon = attributeIcon(
this.hass,
this._config.config,
this._connection.connection,
this._entities,
this.stateObj,
this.attribute,
this.attributeValue
+22
View File
@@ -4,6 +4,8 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { getValueAttribute } from "../common/entity/get_states";
import { formattersContext } from "../data/context";
@customElement("ha-attribute-value")
@@ -56,6 +58,26 @@ class HaAttributeValue extends LitElement {
return html`<pre>${until(yaml, "")}</pre>`;
}
// Options-list attributes (effect_list, preset_modes, …) translated through
// their value attribute, or the main state for lists like hvac_modes.
if (Array.isArray(attributeValue)) {
const domain = computeStateDomain(this.stateObj);
const valueAttribute = getValueAttribute(domain, this.attribute);
if (valueAttribute) {
return attributeValue
.map((item) =>
valueAttribute === "_"
? this._formatters!.formatEntityState(this.stateObj!, item)
: this._formatters!.formatEntityAttributeValue(
this.stateObj!,
valueAttribute,
item
)
)
.join(", ");
}
}
if (this.hideUnit) {
const parts = this._formatters!.formatEntityAttributeValueToParts(
this.stateObj!,
+11 -12
View File
@@ -1,10 +1,12 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { formatNumber } from "../common/number/format_number";
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
import type { HomeAssistant } from "../types";
import { internationalizationContext } from "../data/context";
@customElement("ha-big-number")
export class HaBigNumber extends LitElement {
@@ -15,17 +17,16 @@ export class HaBigNumber extends LitElement {
@property({ attribute: "unit-position" })
public unitPosition: "top" | "bottom" = "top";
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false })
public formatOptions: Intl.NumberFormatOptions = {};
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
protected render() {
const formatted = formatNumber(
this.value,
this.hass?.locale,
this.formatOptions
);
const locale = this._i18n!.locale;
const formatted = formatNumber(this.value, locale, this.formatOptions);
const [integer] = formatted.includes(".")
? formatted.split(".")
: formatted.split(",");
@@ -33,9 +34,7 @@ export class HaBigNumber extends LitElement {
const temperatureDecimal = formatted.replace(integer, "");
const formattedValue = `${this.value}${
this.unit
? `${blankBeforeUnit(this.unit, this.hass?.locale)}${this.unit}`
: ""
this.unit ? `${blankBeforeUnit(this.unit, locale)}${this.unit}` : ""
}`;
const unitBottom = this.unitPosition === "bottom";
+36 -15
View File
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
@@ -7,7 +8,7 @@ import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
import {
CAMERA_SUPPORT_STREAM,
CameraEntityFeature,
type CameraCapabilities,
type CameraEntity,
computeMJPEGStreamUrl,
@@ -17,7 +18,7 @@ import {
STREAM_TYPE_WEB_RTC,
type StreamType,
} from "../data/camera";
import type { HomeAssistant } from "../types";
import { apiContext, configContext, connectionContext } from "../data/context";
import "./ha-hls-player";
import "./ha-web-rtc-player";
@@ -30,7 +31,17 @@ interface Stream {
@customElement("ha-camera-stream")
export class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@state()
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: ContextType<typeof connectionContext>;
@property({ attribute: false }) public stateObj?: CameraEntity;
@@ -58,21 +69,33 @@ export class HaCameraStream extends LitElement {
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
public willUpdate(changedProps: PropertyValues<this>): void {
private _thumbnailApi = memoizeOne(
(
api: ContextType<typeof apiContext>,
connection: ContextType<typeof connectionContext>
) => ({
callWS: api.callWS,
hassUrl: connection.hassUrl,
})
);
public willUpdate(changedProps: PropertyValues): void {
const entityChanged =
changedProps.has("stateObj") &&
this.stateObj &&
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| ContextType<typeof configContext>
| undefined;
const backendStarted =
changedProps.has("hass") &&
this.hass &&
changedProps.has("_config") &&
this._config &&
this.stateObj &&
oldHass &&
this.hass.config.state === STATE_RUNNING &&
oldHass.config?.state !== STATE_RUNNING;
oldConfig &&
this._config.config.state === STATE_RUNNING &&
oldConfig.config?.state !== STATE_RUNNING;
if (entityChanged || backendStarted) {
this._getCapabilities();
@@ -137,7 +160,6 @@ export class HaCameraStream extends LitElement {
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
@@ -153,7 +175,6 @@ export class HaCameraStream extends LitElement {
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
@@ -170,12 +191,12 @@ export class HaCameraStream extends LitElement {
this._capabilities = undefined;
this._hlsStreams = undefined;
this._webRtcStreams = undefined;
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
if (!supportsFeature(this.stateObj!, CameraEntityFeature.STREAM)) {
this._capabilities = { frontend_stream_types: [] };
return;
}
this._capabilities = await fetchCameraCapabilities(
this.hass!,
this._api,
this.stateObj!.entity_id
);
}
@@ -183,7 +204,7 @@ export class HaCameraStream extends LitElement {
private async _getPosterUrl(): Promise<void> {
try {
this._posterUrl = await fetchThumbnailUrlWithCache(
this.hass!,
this._thumbnailApi(this._api, this._connection),
this.stateObj!.entity_id,
this.clientWidth,
this.clientHeight
+30 -10
View File
@@ -1,13 +1,16 @@
import { consume, type ContextType } from "@lit/context";
import type HlsType from "hls.js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import { nextRender } from "../common/util/render-status";
import { fetchStreamUrl } from "../data/camera";
import type { HomeAssistant } from "../types";
import { apiContext, configContext, connectionContext } from "../data/context";
import "./ha-alert";
type HlsLite = Omit<
@@ -17,7 +20,21 @@ type HlsLite = Omit<
@customElement("ha-hls-player")
class HaHLSPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: ContextType<typeof connectionContext>;
@property() public entityid?: string;
@@ -140,7 +157,7 @@ class HaHLSPlayer extends LitElement {
this._cleanUp();
this._resetError();
if (!isComponentLoaded(this.hass.config, "stream")) {
if (!isComponentLoaded(this._config.config, "stream")) {
this._setFatalError("Streaming component is not loaded.");
return;
}
@@ -149,9 +166,12 @@ class HaHLSPlayer extends LitElement {
return;
}
try {
const { url } = await fetchStreamUrl(this.hass!, this.entityid);
const { url } = await fetchStreamUrl(
{ callWS: this._api.callWS, hassUrl: this._connection.hassUrl },
this.entityid
);
this._url = this.hass.hassUrl(url);
this._url = this._connection.hassUrl(url);
this._cleanUp();
this._resetError();
this._startHls();
@@ -184,13 +204,13 @@ class HaHLSPlayer extends LitElement {
if (!hlsSupported) {
this._setFatalError(
this.hass.localize("ui.components.media-browser.video_not_supported")
this._localize("ui.components.media-browser.video_not_supported")
);
return;
}
const useExoPlayer =
this.allowExoPlayer && this.hass.auth.external?.config.hasExoPlayer;
this.allowExoPlayer && this._config.auth.external?.config.hasExoPlayer;
const masterPlaylist = await (await masterPlaylistPromise).text();
if (!this.isConnected) {
@@ -236,7 +256,7 @@ class HaHLSPlayer extends LitElement {
window.addEventListener("resize", this._resizeExoPlayer);
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
this._videoEl.style.visibility = "hidden";
await this.hass!.auth.external!.fireMessage({
await this._config.auth.external!.fireMessage({
type: "exoplayer/play_hls",
payload: {
url,
@@ -250,7 +270,7 @@ class HaHLSPlayer extends LitElement {
return;
}
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
this._config.auth.external!.fireMessage({
type: "exoplayer/resize",
payload: {
left: rect.left,
@@ -362,7 +382,7 @@ class HaHLSPlayer extends LitElement {
}
if (this._exoPlayer) {
window.removeEventListener("resize", this._resizeExoPlayer);
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
this._config.auth.external!.fireMessage({ type: "exoplayer/stop" });
this._exoPlayer = false;
}
if (this._videoEl) {
+20 -8
View File
@@ -12,6 +12,8 @@ import type { HomeAssistantInternationalization } from "../types";
class HaRelativeTime extends ReactiveElement {
@property({ attribute: false }) public datetime?: string | Date;
@property() public format: Intl.RelativeTimeFormatStyle = "long";
@property({ type: Boolean }) public capitalize = false;
@state()
@@ -36,13 +38,15 @@ class HaRelativeTime extends ReactiveElement {
return this;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this._updateRelative();
}
protected update(changedProps: PropertyValues<this>) {
super.update(changedProps);
if (changedProps.has("datetime")) {
if (this.datetime) {
this._startInterval();
} else {
this._clearInterval();
}
}
this._updateRelative();
}
@@ -66,15 +70,23 @@ class HaRelativeTime extends ReactiveElement {
}
if (!this.datetime) {
this.innerHTML = this._i18n.localize("ui.components.relative_time.never");
this.textContent = this._i18n.localize(
"ui.components.relative_time.never"
);
} else {
const date =
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(date, this._i18n.locale);
this.innerHTML = this.capitalize
const relTime = relativeTime(
date,
this._i18n.locale,
undefined,
true,
this.format
);
this.textContent = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;
}
@@ -86,7 +86,10 @@ export class HaDateTimeSelector extends LitElement {
static styles = css`
.input {
display: flex;
align-items: center;
/* Align the input fields by their top edge so the date field's underline
lines up with the time field, since ha-date-input reserves extra space
below for its hint while ha-time-input does not. */
align-items: flex-start;
flex-direction: row;
}
@@ -0,0 +1,32 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-time-format-picker";
@customElement("ha-selector-ui_time_format")
export class HaSelectorUiTimeFormat extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`
<ha-time-format-picker
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
>
</ha-time-format-picker>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-ui_time_format": HaSelectorUiTimeFormat;
}
}
@@ -67,6 +67,7 @@ const LOAD_ELEMENTS = {
ui_action: () => import("./ha-selector-ui-action"),
ui_color: () => import("./ha-selector-ui-color"),
ui_state_content: () => import("./ha-selector-ui-state-content"),
ui_time_format: () => import("./ha-selector-ui-time-format"),
};
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
+136
View File
@@ -0,0 +1,136 @@
import memoizeOne from "memoize-one";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import "./ha-select";
import type { TimestampRenderingFormat } from "../panels/lovelace/components/types";
import { TIMESTAMP_RENDERING_FORMATS } from "../panels/lovelace/components/types";
@customElement("ha-time-format-picker")
export class HaTimeFormatPicker extends LitElement {
@property() public value?: TimestampRenderingFormat;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
private _options = memoizeOne((localize: LocalizeFunc) =>
[{ label: localize("ui.common.auto"), value: "auto" }].concat(
TIMESTAMP_RENDERING_FORMATS.map((format) => ({
label:
localize(`ui.components.time-format-picker.formats.${format}`) ||
format,
value: format,
}))
)
);
private _styleOptions = memoizeOne((localize: LocalizeFunc) => [
{ label: localize("ui.common.auto"), value: "auto" },
{
label: localize("ui.components.time-format-picker.styles.short"),
value: "short",
},
{
label: localize("ui.components.time-format-picker.styles.long"),
value: "long",
},
]);
protected render() {
const type = typeof this.value === "object" ? this.value.type : this.value;
const style = typeof this.value === "object" ? this.value.style : undefined;
return html`
<div class="row">
<ha-select
.label=${this.label ?? ""}
.value=${type || "auto"}
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
@selected=${this._selectChanged}
.options=${this._options(this._localize)}
>
</ha-select>
${this.value
? html`
<ha-select
.label=${this._localize(
"ui.components.time-format-picker.style"
)}
.value=${style || "auto"}
.disabled=${this.disabled}
@selected=${this._styleChanged}
.options=${this._styleOptions(this._localize)}
>
</ha-select>
`
: nothing}
</div>
`;
}
private _selectChanged(ev) {
ev.stopPropagation();
if (ev.detail?.value === "auto" && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
return;
}
if (this.value && typeof this.value === "object" && this.value.style) {
fireEvent(this, "value-changed", {
value: {
type: ev.detail.value,
style: this.value.style,
},
});
return;
}
fireEvent(this, "value-changed", {
value: ev.detail.value,
});
}
private _styleChanged(ev) {
ev.stopPropagation();
const type = typeof this.value === "object" ? this.value.type : this.value;
if (ev.detail?.value === "auto") {
fireEvent(this, "value-changed", {
value: type,
});
return;
}
fireEvent(this, "value-changed", {
value: {
type: type,
style: ev.detail.value,
},
});
}
static styles = css`
.row {
display: flex;
gap: 12px;
}
.row > * {
flex: 1;
min-width: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-time-format-picker": HaTimeFormatPicker;
}
}
+18 -8
View File
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
@@ -13,7 +14,7 @@ import {
webRtcOffer,
type WebRtcOfferEvent,
} from "../data/camera";
import type { HomeAssistant } from "../types";
import { apiContext, connectionContext } from "../data/context";
import "./ha-alert";
/**
@@ -23,7 +24,13 @@ import "./ha-alert";
*/
@customElement("ha-web-rtc-player")
class HaWebRtcPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: ContextType<typeof connectionContext>;
@property() public entityid?: string;
@@ -130,7 +137,7 @@ class HaWebRtcPlayer extends LitElement {
return;
}
if (!this.hass || !this.entityid) {
if (!this._api || !this._connection || !this.entityid) {
return;
}
@@ -141,7 +148,7 @@ class HaWebRtcPlayer extends LitElement {
this._logEvent("start clientConfig");
this._clientConfig = await fetchWebRtcClientConfiguration(
this.hass,
this._api,
this.entityid
);
@@ -230,8 +237,11 @@ class HaWebRtcPlayer extends LitElement {
this._logEvent("start webRtcOffer", offer_sdp);
try {
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
this._handleOfferEvent(event)
this._unsub = webRtcOffer(
this._connection,
this.entityid,
offer_sdp,
(event) => this._handleOfferEvent(event)
);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
@@ -257,7 +267,7 @@ class HaWebRtcPlayer extends LitElement {
this._sessionId = event.session_id;
this._candidatesList.forEach((candidate) =>
addWebRtcCandidate(
this.hass,
this._api,
this.entityid!,
event.session_id,
// toJSON returns RTCIceCandidateInit
@@ -310,7 +320,7 @@ class HaWebRtcPlayer extends LitElement {
if (this._sessionId) {
addWebRtcCandidate(
this.hass,
this._api,
this.entityid,
this._sessionId,
// toJSON returns RTCIceCandidateInit
+8 -7
View File
@@ -1,16 +1,19 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import type { HomeAssistant } from "../../types";
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-state-icon";
@customElement("ha-entity-marker")
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id", reflect: true }) public entityId?: string;
@state()
@consumeEntityState({ entityIdPath: ["entityId"] })
private _stateObj?: HassEntity;
@property({ attribute: "entity-name" }) public entityName?: string;
@property({ attribute: "entity-unit" }) public entityUnit?: string;
@@ -36,9 +39,7 @@ class HaEntityMarker extends LitElement {
})}
></div>`
: this.showIcon && this.entityId
? html`<ha-state-icon
.stateObj=${this.hass?.states[this.entityId]}
></ha-state-icon>`
? html`<ha-state-icon .stateObj=${this._stateObj}></ha-state-icon>`
: !this.entityUnit
? this.entityName
: html`
@@ -128,7 +128,6 @@ export class HaLocationsEditor extends LitElement {
protected render(): TemplateResult {
return html`
<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
+74 -33
View File
@@ -1,4 +1,6 @@
import { consume } from "@lit/context";
import { isToday } from "date-fns";
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
import type {
Circle,
CircleMarker,
@@ -18,6 +20,7 @@ import {
formatTimeWeekday,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
@@ -26,7 +29,22 @@ import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityLocation } from "../../common/entity/get_entity_location";
import { DecoratedMarker } from "../../common/map/decorated_marker";
import { filterXSS } from "../../common/util/xss";
import type { HomeAssistant, ThemeMode } from "../../types";
import {
configContext,
connectionContext,
formattersContext,
internationalizationContext,
statesContext,
uiContext,
} from "../../data/context";
import type {
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantUI,
ThemeMode,
} from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
import "./ha-entity-marker";
@@ -76,7 +94,32 @@ export interface HaMapEntity {
@customElement("ha-map")
export class HaMap extends ReactiveElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HassEntities;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _config!: HassConfig;
@state()
@consume({ context: uiContext, subscribe: true })
private _ui!: HomeAssistantUI;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: HomeAssistantConnection;
@property({ attribute: false }) public entities?: string[] | HaMapEntity[];
@@ -175,17 +218,16 @@ export class HaMap extends ReactiveElement {
return;
}
let autoFitRequired = false;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldStates = changedProps.get("_states") as HassEntities | undefined;
if (changedProps.has("_loaded") || changedProps.has("entities")) {
this._drawEntities();
autoFitRequired = !this._pauseAutoFit;
} else if (this._loaded && oldHass && this.entities) {
} else if (this._loaded && oldStates && this.entities) {
// Check if any state has changed
for (const entity of this.entities) {
if (
oldHass.states[getEntityId(entity)] !==
this.hass!.states[getEntityId(entity)]
oldStates[getEntityId(entity)] !== this._states[getEntityId(entity)]
) {
this._drawEntities();
autoFitRequired = !this._pauseAutoFit;
@@ -219,10 +261,11 @@ export class HaMap extends ReactiveElement {
}, PROGRAMMITIC_FIT_DELAY);
}
const oldUi = changedProps.get("_ui") as HomeAssistantUI | undefined;
if (
!changedProps.has("themeMode") &&
(!changedProps.has("hass") ||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
(!changedProps.has("_ui") ||
(oldUi && oldUi.themes?.darkMode === this._ui.themes?.darkMode))
) {
return;
}
@@ -233,7 +276,7 @@ export class HaMap extends ReactiveElement {
private get _darkMode() {
return (
this.themeMode === "dark" ||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
(this.themeMode === "auto" && Boolean(this._ui?.themes.darkMode))
);
}
@@ -258,8 +301,8 @@ export class HaMap extends ReactiveElement {
this._loading = true;
try {
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map, {
latitude: this.hass?.config.latitude ?? 52.3731339,
longitude: this.hass?.config.longitude ?? 4.8903147,
latitude: this._config?.latitude ?? 52.3731339,
longitude: this._config?.longitude ?? 4.8903147,
zoom: this.zoom,
});
this._updateMapStyle();
@@ -300,7 +343,7 @@ export class HaMap extends ReactiveElement {
if (options?.unpause_autofit) {
this._pauseAutoFit = false;
}
if (!this.leafletMap || !this.Leaflet || !this.hass) {
if (!this.leafletMap || !this.Leaflet || !this._config) {
return;
}
@@ -311,10 +354,7 @@ export class HaMap extends ReactiveElement {
) {
this._isProgrammaticFit = true;
this.leafletMap.setView(
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
new this.Leaflet.LatLng(this._config.latitude, this._config.longitude),
options?.zoom || this.zoom
);
setTimeout(() => {
@@ -351,7 +391,7 @@ export class HaMap extends ReactiveElement {
boundingbox: LatLngExpression[],
options?: { zoom?: number; pad?: number }
) {
if (!this.leafletMap || !this.Leaflet || !this.hass) {
if (!this.leafletMap || !this.Leaflet) {
return;
}
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
@@ -382,32 +422,31 @@ export class HaMap extends ReactiveElement {
if (path.fullDatetime) {
formattedTime = formatDateTime(
point.timestamp,
this.hass.locale,
this.hass.config
this._i18n.locale,
this._config
);
} else if (isToday(point.timestamp)) {
formattedTime = formatTimeWithSeconds(
point.timestamp,
this.hass.locale,
this.hass.config
this._i18n.locale,
this._config
);
} else {
formattedTime = formatTimeWeekday(
point.timestamp,
this.hass.locale,
this.hass.config
this._i18n.locale,
this._config
);
}
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
}
private _drawPaths(): void {
const hass = this.hass;
const map = this.leafletMap;
// eslint-disable-next-line @typescript-eslint/naming-convention
const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) {
if (!this._i18n || !this._config || !map || !Leaflet) {
return;
}
if (this._mapPaths.length) {
@@ -535,12 +574,12 @@ export class HaMap extends ReactiveElement {
}
private _drawEntities(): void {
const hass = this.hass;
const states = this._states;
const map = this.leafletMap;
// eslint-disable-next-line @typescript-eslint/naming-convention
const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) {
if (!states || !map || !Leaflet) {
return;
}
@@ -578,7 +617,7 @@ export class HaMap extends ReactiveElement {
const className = this._darkMode ? "dark" : "light";
for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)];
const stateObj = states[getEntityId(entity)];
if (!stateObj) {
continue;
}
@@ -591,7 +630,7 @@ export class HaMap extends ReactiveElement {
entity_picture: entityPicture,
} = stateObj.attributes;
const location = getEntityLocation(stateObj, hass.states);
const location = getEntityLocation(stateObj, states);
if (!location) {
continue;
}
@@ -648,11 +687,14 @@ export class HaMap extends ReactiveElement {
// create icon
const entityName =
typeof entity !== "string" && entity.label_mode === "state"
? this.hass.formatEntityState(stateObj)
? this._formatters.formatEntityState(stateObj)
: typeof entity !== "string" &&
entity.label_mode === "attribute" &&
entity.attribute !== undefined
? this.hass.formatEntityAttributeValue(stateObj, entity.attribute)
? this._formatters.formatEntityAttributeValue(
stateObj,
entity.attribute
)
: (customTitle ??
title
.split(" ")
@@ -661,7 +703,6 @@ export class HaMap extends ReactiveElement {
.substr(0, 3));
const entityMarker = document.createElement("ha-entity-marker");
entityMarker.hass = this.hass;
entityMarker.showIcon =
typeof entity !== "string" && entity.label_mode === "icon";
entityMarker.entityId = getEntityId(entity);
@@ -674,7 +715,7 @@ export class HaMap extends ReactiveElement {
: "";
entityMarker.entityPicture =
entityPicture && (typeof entity === "string" || !entity.label_mode)
? this.hass.hassUrl(entityPicture)
? this._connection.hassUrl(entityPicture)
: "";
if (typeof entity !== "string") {
entityMarker.entityColor = entity.color;
-1
View File
@@ -26,7 +26,6 @@ export class HaTraceLogbook extends LitElement {
return this.logbookEntries.length
? html`
<ha-logbook-renderer
relative-time
.hass=${this.hass}
.entries=${this.logbookEntries}
.narrow=${this.narrow}
@@ -388,7 +388,6 @@ export class HaTracePathDetails extends LitElement {
return entries.length
? html`
<ha-logbook-renderer
relative-time
.hass=${this.hass}
.entries=${entries}
.narrow=${this.narrow}
+2 -2
View File
@@ -18,7 +18,7 @@ import { toggleAttribute } from "../../common/dom/toggle_attribute";
import { fullEntitiesContext } from "../../data/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { LogbookEntry } from "../../data/logbook";
import { localizeTriggerDescription } from "../../data/logbook";
import { localizeTriggerSource } from "../../data/logbook";
import type {
ChooseAction,
IfAction,
@@ -333,7 +333,7 @@ class ActionRenderer {
: "other",
alias: triggerStep.changed_variables.trigger?.alias,
triggeredPath: triggerStep.path === "trigger" ? "manual" : "trigger",
trigger: localizeTriggerDescription(
trigger: localizeTriggerSource(
this.hass.localize,
this.trace.trigger
),
+1 -1
View File
@@ -1,7 +1,7 @@
import type { HomeAssistant } from "../types";
import type { Selector } from "./selector";
export const enum AITaskEntityFeature {
export enum AITaskEntityFeature {
GENERATE_DATA = 1,
SUPPORT_ATTACHMENTS = 2,
GENERATE_IMAGE = 4,
+2 -2
View File
@@ -18,7 +18,7 @@ import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";
export const enum AlarmControlPanelEntityFeature {
export enum AlarmControlPanelEntityFeature {
ARM_HOME = 1,
ARM_AWAY = 2,
ARM_NIGHT = 4,
@@ -108,7 +108,7 @@ export const supportedAlarmModes = (stateObj: AlarmControlPanelEntity) =>
export const setProtectedAlarmControlPanelMode = async (
element: HTMLElement,
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callService" | "localize" | "callWS">,
stateObj: AlarmControlPanelEntity,
mode: AlarmMode
) => {
+5 -2
View File
@@ -338,7 +338,7 @@ export const runDebugAssistPipeline = (
};
export const runAssistPipeline = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "connection">,
callback: (event: PipelineRunEvent) => void,
options: PipelineRunOptions
) =>
@@ -379,7 +379,10 @@ export const listAssistPipelines = (hass: HomeAssistant) =>
type: "assist_pipeline/pipeline/list",
});
export const getAssistPipeline = (hass: HomeAssistant, pipeline_id?: string) =>
export const getAssistPipeline = (
hass: Pick<HomeAssistant, "callWS">,
pipeline_id?: string
) =>
hass.callWS<AssistPipeline>({
type: "assist_pipeline/pipeline/get",
pipeline_id,
+1 -1
View File
@@ -3,7 +3,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export const enum AssistSatelliteEntityFeature {
export enum AssistSatelliteEntityFeature {
ANNOUNCE = 1,
}
+1 -1
View File
@@ -41,7 +41,7 @@ export const autocompleteLoginFields = (schema: HaFormSchema[]) =>
});
export const getSignedPath = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS">,
path: string
): Promise<SignedPath> => hass.callWS({ type: "auth/sign_path", path });
+1 -1
View File
@@ -377,7 +377,7 @@ export const expandConditionWithShorthand = (
};
export const triggerAutomationActions = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callService">,
entityId: string
) => {
hass.callService("automation", "trigger", {
+1 -1
View File
@@ -54,7 +54,7 @@ export enum RecurrenceRange {
THISANDFUTURE = "THISANDFUTURE",
}
export const enum CalendarEntityFeature {
export enum CalendarEntityFeature {
CREATE_EVENT = 1,
DELETE_EVENT = 2,
UPDATE_EVENT = 4,
+12 -9
View File
@@ -7,14 +7,17 @@ import type { HomeAssistant } from "../types";
import { getSignedPath } from "./auth";
export const CAMERA_ORIENTATIONS = [1, 2, 3, 4, 6, 8];
export const CAMERA_SUPPORT_ON_OFF = 1;
export const CAMERA_SUPPORT_STREAM = 2;
export const STREAM_TYPE_HLS = "hls";
export const STREAM_TYPE_WEB_RTC = "web_rtc";
export type StreamType = typeof STREAM_TYPE_HLS | typeof STREAM_TYPE_WEB_RTC;
export enum CameraEntityFeature {
ON_OFF = 1,
STREAM = 2,
}
interface CameraEntityAttributes extends HassEntityAttributeBase {
model_name: string;
access_token?: string;
@@ -86,7 +89,7 @@ export const computeMJPEGStreamUrl = (
: undefined;
export const fetchThumbnailUrlWithCache = async (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
entityId: string,
width: number,
height: number
@@ -102,7 +105,7 @@ export const fetchThumbnailUrlWithCache = async (
};
export const fetchThumbnailUrl = async (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
entityId: string
) => {
const path = await getSignedPath(hass, `/api/camera_proxy/${entityId}`);
@@ -110,7 +113,7 @@ export const fetchThumbnailUrl = async (
};
export const fetchStreamUrl = async (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
entityId: string,
format?: "hls"
) => {
@@ -128,7 +131,7 @@ export const fetchStreamUrl = async (
};
export const webRtcOffer = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "connection">,
entity_id: string,
offer: string,
callback: (event: WebRtcOfferEvent) => void
@@ -140,7 +143,7 @@ export const webRtcOffer = (
});
export const addWebRtcCandidate = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS">,
entity_id: string,
session_id: string,
candidate: RTCIceCandidateInit
@@ -186,7 +189,7 @@ export interface CameraCapabilities {
}
export const fetchCameraCapabilities = async (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS">,
entity_id: string
) =>
hass.callWS<CameraCapabilities>({ type: "camera/capabilities", entity_id });
@@ -197,7 +200,7 @@ export interface WebRTCClientConfiguration {
}
export const fetchWebRtcClientConfiguration = async (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS">,
entityId: string
) =>
hass.callWS<WebRTCClientConfiguration>({
+1 -1
View File
@@ -68,7 +68,7 @@ export type ClimateEntity = HassEntityBase & {
};
};
export const enum ClimateEntityFeature {
export enum ClimateEntityFeature {
TARGET_TEMPERATURE = 1,
TARGET_TEMPERATURE_RANGE = 2,
TARGET_HUMIDITY = 4,
+1 -1
View File
@@ -1,7 +1,7 @@
import { ensureArray } from "../common/array/ensure-array";
import type { HomeAssistant } from "../types";
export const enum ConversationEntityFeature {
export enum ConversationEntityFeature {
CONTROL = 1,
}
+4 -4
View File
@@ -4,10 +4,10 @@ import type {
} from "home-assistant-js-websocket";
import { stateActive } from "../common/entity/state_active";
import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistant } from "../types";
import type { HomeAssistantFormatters } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export const enum CoverEntityFeature {
export enum CoverEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
@@ -122,7 +122,7 @@ export interface CoverEntity extends HassEntityBase {
export function computeCoverPositionStateDisplay(
stateObj: CoverEntity,
hass: HomeAssistant,
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
position?: number
) {
const statePosition = stateActive(stateObj)
@@ -133,7 +133,7 @@ export function computeCoverPositionStateDisplay(
const currentPosition = position ?? statePosition;
return currentPosition && currentPosition !== 100
? hass.formatEntityAttributeValue(
? formatEntityAttributeValue(
stateObj,
// Always use position as it's the same formatting as tilt position
"current_position",
+3 -3
View File
@@ -1,14 +1,14 @@
import type { HassEntityBase } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { HomeAssistantApi } from "../types";
export const stateToIsoDateString = (entityState: HassEntityBase) =>
`${entityState}T00:00:00`;
export const setDateValue = (
hass: HomeAssistant,
callService: HomeAssistantApi["callService"],
entityId: string,
date: string | undefined = undefined
) => {
const param = { entity_id: entityId, date };
hass.callService("date", "set_value", param);
callService("date", "set_value", param);
};
+3 -3
View File
@@ -1,11 +1,11 @@
import type { HomeAssistant } from "../types";
import type { HomeAssistantApi } from "../types";
export const setDateTimeValue = (
hass: HomeAssistant,
callService: HomeAssistantApi["callService"],
entityId: string,
datetime: Date
) => {
hass.callService("datetime", "set_value", {
callService("datetime", "set_value", {
entity_id: entityId,
datetime: datetime.toISOString(),
});
+8 -9
View File
@@ -211,14 +211,14 @@ export interface EntityRegistryEntryUpdateParams {
const batteryPriorities = ["sensor", "binary_sensor"];
export const findBatteryEntity = <T extends { entity_id: string }>(
hass: HomeAssistant,
states: HomeAssistant["states"],
entities: T[]
): T | undefined => {
const batteryEntities = entities
.filter(
(entity) =>
hass.states[entity.entity_id] &&
hass.states[entity.entity_id].attributes.device_class === "battery" &&
states[entity.entity_id] &&
states[entity.entity_id].attributes.device_class === "battery" &&
batteryPriorities.includes(computeDomain(entity.entity_id))
)
.sort(
@@ -234,14 +234,13 @@ export const findBatteryEntity = <T extends { entity_id: string }>(
};
export const findBatteryChargingEntity = <T extends { entity_id: string }>(
hass: HomeAssistant,
states: HomeAssistant["states"],
entities: T[]
): T | undefined =>
entities.find(
(entity) =>
hass.states[entity.entity_id] &&
hass.states[entity.entity_id].attributes.device_class ===
"battery_charging"
states[entity.entity_id] &&
states[entity.entity_id].attributes.device_class === "battery_charging"
);
export const computeEntityRegistryName = (
@@ -259,7 +258,7 @@ export const computeEntityRegistryName = (
};
export const getExtendedEntityRegistryEntry = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS">,
entityId: string
): Promise<ExtEntityRegistryEntry> =>
hass.callWS({
@@ -277,7 +276,7 @@ export const getExtendedEntityRegistryEntries = (
});
export const updateEntityRegistryEntry = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS">,
entityId: string,
updates: Partial<EntityRegistryEntryUpdateParams>
): Promise<UpdateEntityRegistryEntryResult> =>
+3 -3
View File
@@ -12,7 +12,7 @@ import type {
import { stateActive } from "../common/entity/state_active";
import type { HomeAssistant } from "../types";
export const enum FanEntityFeature {
export enum FanEntityFeature {
SET_SPEED = 1,
OSCILLATE = 2,
DIRECTION = 4,
@@ -100,7 +100,7 @@ export const FAN_SPEED_COUNT_MAX_FOR_BUTTONS = 4;
export function computeFanSpeedStateDisplay(
stateObj: FanEntity,
hass: HomeAssistant,
formatters: Pick<HomeAssistant, "formatEntityAttributeValue">,
speed?: number
) {
const percentage = stateActive(stateObj)
@@ -109,7 +109,7 @@ export function computeFanSpeedStateDisplay(
const currentSpeed = speed ?? percentage;
return currentSpeed
? hass.formatEntityAttributeValue(
? formatters.formatEntityAttributeValue(
stateObj,
"percentage",
Math.round(currentSpeed)
+1 -1
View File
@@ -20,7 +20,7 @@ export type HumidifierEntity = HassEntityBase & {
};
};
export const enum HumidifierEntityFeature {
export enum HumidifierEntityFeature {
MODES = 1,
}
+10 -8
View File
@@ -39,6 +39,7 @@ import {
mdiMicrophoneMessage,
mdiMotionSensor,
mdiPalette,
mdiRadioTower,
mdiRayVertex,
mdiRemote,
mdiRobot,
@@ -52,7 +53,6 @@ import {
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitch,
mdiVideoInputAntenna,
mdiWater,
mdiWaterPercent,
mdiWeatherPartlyCloudy,
@@ -129,7 +129,7 @@ export const FALLBACK_DOMAIN_ICONS = {
plant: mdiFlower,
power: mdiFlash,
proximity: mdiAppleSafari,
radio_frequency: mdiVideoInputAntenna,
radio_frequency: mdiRadioTower,
remote: mdiRemote,
scene: mdiPalette,
schedule: mdiCalendarClock,
@@ -548,7 +548,9 @@ const getEntityIcon = async (
};
export const attributeIcon = async (
hass: HomeAssistant,
hassConfig: HomeAssistant["config"],
hassConnection: HomeAssistant["connection"],
entities: HomeAssistant["entities"],
state: HassEntity,
attribute: string,
attributeValue?: string
@@ -556,7 +558,7 @@ export const attributeIcon = async (
let icon: string | undefined;
const domain = computeStateDomain(state);
const deviceClass = state.attributes.device_class;
const entity = hass.entities?.[state.entity_id] as
const entity = entities[state.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
const platform = entity?.platform;
@@ -567,8 +569,8 @@ export const attributeIcon = async (
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(
hass.config,
hass.connection,
hassConfig,
hassConnection,
platform
);
if (platformIcons) {
@@ -580,8 +582,8 @@ export const attributeIcon = async (
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(
hass.connection,
hass.config,
hassConnection,
hassConfig,
domain
);
if (entityComponentIcons) {
+134
View File
@@ -0,0 +1,134 @@
import { computeDeviceName } from "../common/entity/compute_device_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeEntityName } from "../common/entity/compute_entity_name";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE, UNKNOWN } from "./entity/entity";
// The infrared integration is an entity-type integration: its emitter and
// receiver entities live in the `infrared` domain (their registry platform is
// the providing integration, e.g. broadlink/esphome).
const INFRARED_DOMAIN = "infrared";
export type InfraredProxyType = "emitter" | "receiver";
export type InfraredDeviceType = InfraredProxyType | "both";
export interface InfraredDevice {
id: string;
device_id: string | null;
name: string;
type: InfraredDeviceType;
online: boolean;
// Most recent last-used timestamp (entity state) across the device's
// entities, as an ISO string. Undefined when never used.
last_used?: string;
entity_ids: string[];
}
interface InfraredProxyEntity {
entity_id: string;
device_id: string | null;
name: string;
type: InfraredProxyType;
online: boolean;
last_used?: string;
}
// Collect the infrared proxy entities from the entity registry. A proxy is an
// entity in the `infrared` domain, classified as emitter or receiver by its
// device class.
const computeInfraredProxies = (
entities: HomeAssistant["entities"],
states: HomeAssistant["states"],
devices: HomeAssistant["devices"]
): InfraredProxyEntity[] => {
const proxies: InfraredProxyEntity[] = [];
for (const entry of Object.values(entities)) {
if (computeDomain(entry.entity_id) !== INFRARED_DOMAIN) {
continue;
}
const stateObj = states[entry.entity_id];
const deviceClass = stateObj?.attributes.device_class;
if (deviceClass !== "emitter" && deviceClass !== "receiver") {
continue;
}
const online = stateObj.state !== UNAVAILABLE;
// The entity state holds the timestamp the proxy was last used (or
// unknown/unavailable when it never has been).
let last_used: string | undefined;
if (stateObj.state !== UNAVAILABLE && stateObj.state !== UNKNOWN) {
const time = new Date(stateObj.state).getTime();
if (!isNaN(time)) {
last_used = stateObj.state;
}
}
proxies.push({
entity_id: entry.entity_id,
device_id: entry.device_id ?? null,
name: computeEntityName(stateObj, entities, devices) || entry.entity_id,
type: deviceClass,
online,
last_used,
});
}
return proxies;
};
// Group the proxy entities by device. A device exposing both an emitter and a
// receiver entity is reported as type "both".
export const computeInfraredDevices = (
entities: HomeAssistant["entities"],
states: HomeAssistant["states"],
devices: HomeAssistant["devices"]
): InfraredDevice[] => {
const proxies = computeInfraredProxies(entities, states, devices);
const groups = new Map<string, InfraredProxyEntity[]>();
for (const proxy of proxies) {
const key = proxy.device_id ?? `entity:${proxy.entity_id}`;
const group = groups.get(key);
if (group) {
group.push(proxy);
} else {
groups.set(key, [proxy]);
}
}
return Array.from(groups.values(), (group) => {
const hasEmitter = group.some((p) => p.type === "emitter");
const hasReceiver = group.some((p) => p.type === "receiver");
const type: InfraredDeviceType =
hasEmitter && hasReceiver ? "both" : hasEmitter ? "emitter" : "receiver";
const online = group.some((p) => p.online);
// Across a device's entities, keep the most recent valid timestamp.
let last_used: string | undefined;
for (const p of group) {
if (
p.last_used &&
(!last_used ||
new Date(p.last_used).getTime() > new Date(last_used).getTime())
) {
last_used = p.last_used;
}
}
const { device_id } = group[0];
const device = device_id ? devices[device_id] : undefined;
const name = (device && computeDeviceName(device)) || group[0].name;
return {
id: device_id ?? group[0].entity_id,
device_id,
name,
type,
online,
last_used,
entity_ids: group.map((p) => p.entity_id),
};
});
};
+3 -3
View File
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
export interface InputDateTime {
id: string;
@@ -32,13 +32,13 @@ export const stateToIsoDateString = (entityState: HassEntity) =>
)}`;
export const setInputDateTimeValue = (
hass: HomeAssistant,
callService: HomeAssistantApi["callService"],
entityId: string,
time: string | undefined = undefined,
date: string | undefined = undefined
) => {
const param = { entity_id: entityId, time, date };
hass.callService("input_datetime", "set_datetime", param);
callService("input_datetime", "set_datetime", param);
};
export const fetchInputDateTime = (hass: HomeAssistant) =>
+1
View File
@@ -7,6 +7,7 @@ import type { HomeAssistant } from "../types";
export const integrationsWithPanel = {
bluetooth: "config/bluetooth",
dhcp: "config/dhcp",
infrared: "config/infrared",
matter: "config/matter",
mqtt: "config/mqtt",
ssdp: "config/ssdp",
+1 -1
View File
@@ -11,7 +11,7 @@ export type LawnMowerEntityState =
| "docked"
| "error";
export const enum LawnMowerEntityFeature {
export enum LawnMowerEntityFeature {
START_MOWING = 1,
PAUSE = 2,
DOCK = 4,
+1 -1
View File
@@ -4,7 +4,7 @@ import type {
} from "home-assistant-js-websocket";
import { temperature2rgb } from "../common/color/convert-light-color";
export const enum LightEntityFeature {
export enum LightEntityFeature {
EFFECT = 4,
FLASH = 8,
TRANSITION = 32,
+2 -2
View File
@@ -7,7 +7,7 @@ import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
export const enum LockEntityFeature {
export enum LockEntityFeature {
OPEN = 1,
}
@@ -80,7 +80,7 @@ export function canUnlock(stateObj: LockEntity) {
export const callProtectedLockService = async (
element: HTMLElement,
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callService" | "localize" | "callWS">,
stateObj: LockEntity,
service: ProtectedLockService
) => {
+78 -195
View File
@@ -1,15 +1,9 @@
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
BINARY_STATE_OFF,
BINARY_STATE_ON,
DOMAINS_WITH_DYNAMIC_PICTURE,
} from "../common/const";
import { DOMAINS_WITH_DYNAMIC_PICTURE } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { autoCaseNoun } from "../common/translations/auto_case_noun";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE, UNKNOWN } from "./entity/entity";
import { isNumericEntity } from "./history";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
@@ -29,7 +23,7 @@ export interface LogbookEntry {
message?: string;
entity_id?: string;
icon?: string;
source?: string; // The trigger source
source?: string; // The trigger source (English phrase, parsed for the cause)
domain?: string;
state?: string; // The state of the entity
// Context data
@@ -50,23 +44,27 @@ export interface LogbookEntry {
// Localization mapping for all the triggers in core
// in homeassistant.components.homeassistant.triggers
//
type TriggerPhraseKeys =
| "triggered_by_numeric_state_of"
| "triggered_by_state_of"
| "triggered_by_event"
| "triggered_by_time"
| "triggered_by_time_pattern"
| "triggered_by_homeassistant_stopping"
| "triggered_by_homeassistant_starting";
// Keys are the bare translation keys under `ui.components.logbook`.
//
type TriggerPhraseKey =
| "numeric_state_of"
| "state_of"
| "event"
| "time_pattern"
| "time"
| "homeassistant_stopping"
| "homeassistant_starting";
const triggerPhrases: Record<TriggerPhraseKeys, string> = {
triggered_by_numeric_state_of: "numeric state of", // number state trigger
triggered_by_state_of: "state of", // state trigger
triggered_by_event: "event", // event trigger
triggered_by_time_pattern: "time pattern", // time trigger
triggered_by_time: "time", // time trigger
triggered_by_homeassistant_stopping: "Home Assistant stopping", // stop event
triggered_by_homeassistant_starting: "Home Assistant starting", // start event
// Order matters: "time pattern" must be tested before "time" because the
// source phrase is matched with `startsWith`.
const triggerPhrases: Record<TriggerPhraseKey, string> = {
numeric_state_of: "numeric state of", // number state trigger
state_of: "state of", // state trigger
event: "event", // event trigger
time_pattern: "time pattern", // time trigger
time: "time", // time trigger
homeassistant_stopping: "Home Assistant stopping", // stop event
homeassistant_starting: "Home Assistant starting", // start event
};
export const getLogbookDataForContext = async (
@@ -158,215 +156,100 @@ export const createHistoricState = (
state: state,
attributes: {
// Rebuild the historical state by copying static attributes only
device_class: currentStateObj?.attributes.device_class,
source_type: currentStateObj?.attributes.source_type,
has_date: currentStateObj?.attributes.has_date,
has_time: currentStateObj?.attributes.has_time,
device_class: currentStateObj.attributes.device_class,
unit_of_measurement: currentStateObj.attributes.unit_of_measurement,
state_class: currentStateObj.attributes.state_class,
options: currentStateObj.attributes.options,
source_type: currentStateObj.attributes.source_type,
has_date: currentStateObj.attributes.has_date,
has_time: currentStateObj.attributes.has_time,
// We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering,
// as they would present a false state in the log (played media right now vs actual historic data).
entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has(
computeDomain(currentStateObj.entity_id)
)
? undefined
: currentStateObj?.attributes.entity_picture_local,
: currentStateObj.attributes.entity_picture_local,
entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has(
computeDomain(currentStateObj.entity_id)
)
? undefined
: currentStateObj?.attributes.entity_picture,
: currentStateObj.attributes.entity_picture,
},
}) as unknown as HassEntity;
// Localize a backend trigger `source` phrase (e.g. "state of sensor.x") by
// translating the leading phrase while keeping the entity id. The automation
// trace timeline frames it with its own "triggered by" wording, so we only
// translate the bare description here.
export const localizeTriggerSource = (
localize: LocalizeFunc,
source: string
) => {
for (const triggerPhraseKey of Object.keys(
triggerPhrases
) as TriggerPhraseKeys[]) {
const phrase = triggerPhrases[triggerPhraseKey];
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
const phrase = triggerPhrases[key];
if (source.startsWith(phrase)) {
return source.replace(
phrase,
`${localize(`ui.components.logbook.${triggerPhraseKey}`)}`
);
return source.replace(phrase, localize(`ui.components.logbook.${key}`));
}
}
return source;
};
// Mapping from a phrase key to the bare-phrase translation key (without the
// "triggered by" prefix), used by localizeTriggerDescription below.
const triggerDescriptionKeys: Record<
TriggerPhraseKeys,
| "numeric_state_of"
| "state_of"
| "event"
export type TriggerPlatform =
| "state"
| "numeric_state"
| "time"
| "time_pattern"
| "homeassistant_stopping"
| "homeassistant_starting"
> = {
triggered_by_numeric_state_of: "numeric_state_of",
triggered_by_state_of: "state_of",
triggered_by_event: "event",
triggered_by_time_pattern: "time_pattern",
triggered_by_time: "time",
triggered_by_homeassistant_stopping: "homeassistant_stopping",
triggered_by_homeassistant_starting: "homeassistant_starting",
| "event"
| "homeassistant";
// Maps the English `triggerPhrases` to automation trigger platforms, so the
// feed can reuse the editor's trigger-type labels instead of dedicated strings.
const triggerPlatform: Record<TriggerPhraseKey, TriggerPlatform> = {
numeric_state_of: "numeric_state",
state_of: "state",
event: "event",
time_pattern: "time_pattern",
time: "time",
homeassistant_stopping: "homeassistant",
homeassistant_starting: "homeassistant",
};
// Like localizeTriggerSource, but returns just the bare localized trigger
// description (without the "triggered by" prefix). Used where the surrounding
// template already supplies its own "triggered by" wording.
export const localizeTriggerDescription = (
localize: LocalizeFunc,
source: string
) => {
for (const triggerPhraseKey of Object.keys(
triggerPhrases
) as TriggerPhraseKeys[]) {
const phrase = triggerPhrases[triggerPhraseKey];
if (source.startsWith(phrase)) {
const bareKey = triggerDescriptionKeys[triggerPhraseKey];
return source.replace(
phrase,
`${localize(`ui.components.logbook.${bareKey}`)}`
);
export interface ParsedTriggerSource {
platform?: TriggerPlatform;
entityId?: string;
}
// Best-effort parse of the backend's English trigger `source` (e.g. "numeric
// state of sensor.x", "time pattern") into a platform + triggering entity.
// Temporary bridge until the backend sends the trigger structurally.
export const parseTriggerSource = (source: string): ParsedTriggerSource => {
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
const phrase = triggerPhrases[key];
if (!source.startsWith(phrase)) {
continue;
}
const rest = source.slice(phrase.length).trim();
const entityId = /^[a-z_]+\.[a-z0-9_]+$/.test(rest) ? rest : undefined;
return { platform: triggerPlatform[key], entityId };
}
return source;
return {};
};
export const localizeStateMessage = (
hass: HomeAssistant,
localize: LocalizeFunc,
state: string,
stateObj: HassEntity,
domain: string
): string => {
switch (domain) {
case "device_tracker":
case "person":
if (state === "not_home") {
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
}
if (state === "home") {
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
}
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_state`, { state });
case "sun":
return state === "above_horizon"
? localize(`${LOGBOOK_LOCALIZE_PATH}.rose`)
: localize(`${LOGBOOK_LOCALIZE_PATH}.set`);
case "binary_sensor": {
const isOn = state === BINARY_STATE_ON;
const isOff = state === BINARY_STATE_OFF;
const device_class = stateObj.attributes.device_class;
if (device_class && (isOn || isOff)) {
return (
localize(
`${LOGBOOK_LOCALIZE_PATH}.${isOn ? "detected_device_classes" : "cleared_device_classes"}.${device_class}`,
{
device_class: autoCaseNoun(
localize(
`component.binary_sensor.entity_component.${device_class}.name`
) || device_class,
hass.language
),
}
) ||
// If there's no key for a specific device class, fallback to generic string
localize(
`${LOGBOOK_LOCALIZE_PATH}.${isOn ? "detected_device_class" : "cleared_device_class"}`,
{
device_class: autoCaseNoun(
localize(
`component.binary_sensor.entity_component.${device_class}.name`
) || device_class,
hass.language
),
}
)
);
}
break;
}
case "cover":
switch (state) {
case "open":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
case "opening":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "closing":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`);
case "closed":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
}
break;
case "event": {
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
// TODO: This is not working yet, as we don't get historic attribute values
const event_type = hass
.formatEntityAttributeValue(stateObj, "event_type")
?.toString();
if (!event_type) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
}
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event`, {
event_type: autoCaseNoun(event_type, hass.language),
});
}
case "lock":
switch (state) {
case "unlocked":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
case "locking":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_locking`);
case "unlocking":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_unlocking`);
case "opening":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "open":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opened`);
case "locked":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
case "jammed":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_jammed`);
}
break;
// Events expose a timestamp as their state, which has no meaningful display
// value, so keep a dedicated phrase.
if (domain === "event") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
}
if (state === BINARY_STATE_ON) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_on`);
}
if (state === BINARY_STATE_OFF) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`);
}
if (state === UNKNOWN) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unknown`);
}
if (state === UNAVAILABLE) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`);
}
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, {
state: stateObj ? hass.formatEntityState(stateObj, state) : state,
});
// Every other domain reuses the backend state translation, so the logbook
// speaks the same vocabulary as the rest of the UI.
return hass.formatEntityState(stateObj, state);
};
export const filterLogbookCompatibleEntities = (entity) => {
+3 -3
View File
@@ -82,7 +82,7 @@ export interface MediaPlayerEntity extends HassEntityBase {
| "buffering";
}
export const enum MediaPlayerEntityFeature {
export enum MediaPlayerEntityFeature {
PAUSE = 1,
SEEK = 2,
VOLUME_SET = 4,
@@ -481,7 +481,7 @@ export const setMediaPlayerVolume = (
hass.callService("media_player", "volume_set", { entity_id, volume_level });
export const handleMediaControlClick = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callService">,
stateObj: MediaPlayerEntity,
action: string
) =>
@@ -509,7 +509,7 @@ export const handleMediaControlClick = (
);
export const mediaPlayerPlayMedia = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callService">,
entity_id: string,
media_content_id: string,
media_content_type: string,
+3
View File
@@ -0,0 +1,3 @@
export enum NotifyEntityFeature {
TITLE = 1,
}
+22
View File
@@ -0,0 +1,22 @@
import type { HomeAssistant } from "../types";
export const DOMAIN = "radio_frequency";
export interface RadioFrequencyTransmitter {
entity_id: string;
device_id: string | null;
config_entry_id: string | null;
supported_frequency_ranges: [number, number][];
supported_modulations: string[];
}
interface RadioFrequencyTransmitterList {
transmitters: RadioFrequencyTransmitter[];
}
export const fetchRadioFrequencyTransmitters = (
hass: HomeAssistant
): Promise<RadioFrequencyTransmitterList> =>
hass.callWS({
type: "radio_frequency/list",
});
+5 -3
View File
@@ -3,9 +3,11 @@ import type {
HassEntityBase,
} from "home-assistant-js-websocket";
export const REMOTE_SUPPORT_LEARN_COMMAND = 1;
export const REMOTE_SUPPORT_DELETE_COMMAND = 2;
export const REMOTE_SUPPORT_ACTIVITY = 4;
export enum RemoteEntityFeature {
LEARN_COMMAND = 1,
DELETE_COMMAND = 2,
ACTIVITY = 4,
}
export type RemoteEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
+28 -7
View File
@@ -1,6 +1,7 @@
import type {
HassEntityAttributeBase,
HassEntityBase,
HassServices,
HassServiceTarget,
} from "home-assistant-js-websocket";
import type { Describe } from "superstruct";
@@ -104,6 +105,9 @@ export interface Field {
selector?: any;
}
const getScriptFields = (services: HassServices, entityId: string) =>
services.script[computeObjectId(entityId)]?.fields;
interface BaseAction {
alias?: string;
note?: string;
@@ -391,31 +395,41 @@ export const getActionType = (action: Action): ActionType => {
export const isAction = (value: unknown): value is Action =>
getActionType(value as Action) !== "unknown";
export const hasScriptFields = (
hass: HomeAssistant,
export const hasScriptFieldsForServices = (
services: HassServices,
entityId: string
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
const fields = getScriptFields(services, entityId);
return fields !== undefined && Object.keys(fields).length > 0;
};
export const hasRequiredScriptFields = (
export const hasScriptFields = (
hass: HomeAssistant,
entityId: string
): boolean => hasScriptFieldsForServices(hass.services, entityId);
export const hasRequiredScriptFieldsForServices = (
services: HassServices,
entityId: string
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
const fields = getScriptFields(services, entityId);
return (
fields !== undefined &&
Object.values(fields).some((field) => field.required)
);
};
export const requiredScriptFieldsFilled = (
export const hasRequiredScriptFields = (
hass: HomeAssistant,
entityId: string
): boolean => hasRequiredScriptFieldsForServices(hass.services, entityId);
export const requiredScriptFieldsFilledForServices = (
services: HassServices,
entityId: string,
data?: Record<string, any>
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
const fields = getScriptFields(services, entityId);
if (fields === undefined || Object.keys(fields).length === 0) {
return true;
}
@@ -430,6 +444,13 @@ export const requiredScriptFieldsFilled = (
});
};
export const requiredScriptFieldsFilled = (
hass: HomeAssistant,
entityId: string,
data?: Record<string, any>
): boolean =>
requiredScriptFieldsFilledForServices(hass.services, entityId, data);
export const migrateAutomationAction = (
action: Action | Action[]
): Action | Action[] => {
+5
View File
@@ -82,6 +82,7 @@ export type Selector =
| UiActionSelector
| UiColorSelector
| UiStateContentSelector
| UiTimeFormatSelector
| BackupLocationSelector;
export interface ActionSelector {
@@ -601,6 +602,10 @@ export interface UiStateContentSelector {
} | null;
}
export interface UiTimeFormatSelector {
ui_time_format: {} | null;
}
export interface EntityNameSelector {
entity_name: {
entity_id?: string;
+7 -7
View File
@@ -1,7 +1,7 @@
export const SirenEntityFeature = {
TURN_ON: 1,
TURN_OFF: 2,
TONES: 4,
VOLUME_SET: 8,
DURATION: 16,
};
export enum SirenEntityFeature {
TURN_ON = 1,
TURN_OFF = 2,
TONES = 4,
VOLUME_SET = 8,
DURATION = 16,
}
+3 -3
View File
@@ -1,10 +1,10 @@
import type { HomeAssistant } from "../types";
import type { HomeAssistantApi } from "../types";
export const setTimeValue = (
hass: HomeAssistant,
callService: HomeAssistantApi["callService"],
entityId: string,
time: string | undefined = undefined
) => {
const param = { entity_id: entityId, time: time };
hass.callService("time", "set_value", param);
callService("time", "set_value", param);
};
+1 -1
View File
@@ -31,7 +31,7 @@ export interface TodoItem {
completed?: string | null;
}
export const enum TodoListEntityFeature {
export enum TodoListEntityFeature {
CREATE_TODO_ITEM = 1,
DELETE_TODO_ITEM = 2,
UPDATE_TODO_ITEM = 4,
+1 -1
View File
@@ -15,7 +15,7 @@ export type VacuumEntityState =
| "returning"
| "error";
export const enum VacuumEntityFeature {
export enum VacuumEntityFeature {
TURN_ON = 1,
TURN_OFF = 2,
PAUSE = 4,
+4 -4
View File
@@ -4,10 +4,10 @@ import type {
} from "home-assistant-js-websocket";
import { stateActive } from "../common/entity/state_active";
import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistant } from "../types";
import type { HomeAssistantFormatters } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export const enum ValveEntityFeature {
export enum ValveEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
@@ -78,7 +78,7 @@ export interface ValveEntity extends HassEntityBase {
export function computeValvePositionStateDisplay(
stateObj: ValveEntity,
hass: HomeAssistant,
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
position?: number
) {
const statePosition = stateActive(stateObj)
@@ -88,7 +88,7 @@ export function computeValvePositionStateDisplay(
const currentPosition = position ?? statePosition;
return currentPosition && currentPosition !== 100
? hass.formatEntityAttributeValue(
? formatEntityAttributeValue(
stateObj,
"current_position",
Math.round(currentPosition)
+1 -1
View File
@@ -3,7 +3,7 @@ import type {
HassEntityBase,
} from "home-assistant-js-websocket";
export const enum WaterHeaterEntityFeature {
export enum WaterHeaterEntityFeature {
TARGET_TEMPERATURE = 1,
OPERATION_MODE = 2,
AWAY_MODE = 4,
+13 -11
View File
@@ -38,10 +38,11 @@ import {
} from "../common/const";
import { supportsFeature } from "../common/entity/supports-feature";
import { round } from "../common/number/round";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-svg-icon";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantFormatters } from "../types";
export const enum WeatherEntityFeature {
export enum WeatherEntityFeature {
FORECAST_DAILY = 1,
FORECAST_HOURLY = 2,
FORECAST_TWICE_DAILY = 4,
@@ -220,19 +221,20 @@ const getWindBearing = (bearing: number | string): string => {
};
export const getWind = (
hass: HomeAssistant,
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
localize: LocalizeFunc,
stateObj: WeatherEntity,
speed?: number,
bearing?: number | string
): string => {
const speedText =
speed !== undefined && speed !== null
? hass.formatEntityAttributeValue(stateObj, "wind_speed", speed)
? formatEntityAttributeValue(stateObj, "wind_speed", speed)
: "-";
if (bearing !== undefined && bearing !== null) {
const cardinalDirection = getWindBearing(bearing);
return `${speedText} (${
hass.localize(
localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection
})`;
@@ -278,13 +280,13 @@ export const getWeatherUnit = (
};
export const getSecondaryWeatherAttribute = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "formatEntityAttributeValue" | "localize">,
stateObj: WeatherEntity,
forecast: ForecastAttribute[],
temperatureFractionDigits?: number
): TemplateResult | undefined => {
const extrema = getWeatherExtrema(
hass,
hass.formatEntityAttributeValue,
stateObj,
forecast,
temperatureFractionDigits
@@ -320,13 +322,13 @@ export const getSecondaryWeatherAttribute = (
? html`
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
`
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
: hass.localize(`ui.card.weather.attributes.${attribute}`)}
${hass.formatEntityAttributeValue(stateObj, attribute, roundedValue)}
`;
};
const getWeatherExtrema = (
hass: HomeAssistant,
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
stateObj: WeatherEntity,
forecast: ForecastAttribute[],
temperatureFractionDigits?: number
@@ -369,11 +371,11 @@ const getWeatherExtrema = (
return html`
${tempHigh
? hass.formatEntityAttributeValue(stateObj, "temperature", tempHigh)
? formatEntityAttributeValue(stateObj, "temperature", tempHigh)
: ""}
${tempLow && tempHigh ? " / " : ""}
${tempLow
? hass.formatEntityAttributeValue(stateObj, "temperature", tempLow)
? formatEntityAttributeValue(stateObj, "temperature", tempLow)
: ""}
`;
};
@@ -1,11 +1,16 @@
import { consume } from "@lit/context";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { transform } from "../../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-control-button";
import { apiContext, configContext } from "../../../../data/context";
import type { CoverEntity } from "../../../../data/cover";
import {
DEFAULT_COVER_FAVORITE_POSITIONS,
@@ -20,7 +25,11 @@ import type {
ExtEntityRegistryEntry,
} from "../../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConfig,
} from "../../../../types";
import {
showConfirmationDialog,
showPromptDialog,
@@ -46,7 +55,20 @@ const favoriteKindFromEvent = (ev: Event): FavoriteKind =>
@customElement("ha-more-info-cover-favorite-positions")
export class HaMoreInfoCoverFavoritePositions extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HomeAssistant["user"]>({
transformer: ({ user }) => user,
})
private _user!: HomeAssistant["user"];
@property({ attribute: false }) public stateObj!: CoverEntity;
@@ -85,7 +107,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
key: FavoriteLocalizeKey,
values?: Record<string, string | number>
): string {
return this.hass.localize(
return this._localize(
`ui.dialogs.more_info_control.cover.${kind === "position" ? "favorite_position" : "favorite_tilt_position"}.${key}`,
values
);
@@ -124,7 +146,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
}
const result = await updateEntityRegistryEntry(
this.hass,
this._api,
this.entry.entity_id,
{
options_domain: "cover",
@@ -169,14 +191,14 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
}
if (kind === "position") {
this.hass.callService("cover", "set_cover_position", {
this._api.callService("cover", "set_cover_position", {
entity_id: this.stateObj.entity_id,
position: favorite,
});
return;
}
this.hass.callService("cover", "set_cover_tilt_position", {
this._api.callService("cover", "set_cover_tilt_position", {
entity_id: this.stateObj.entity_id,
tilt_position: favorite,
});
@@ -191,7 +213,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
kind,
value === undefined ? "add_title" : "edit_title"
),
inputLabel: this.hass.localize(
inputLabel: this._localize(
kind === "position"
? "ui.card.cover.position"
: "ui.card.cover.tilt_position"
@@ -311,7 +333,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
const { action, index } = ev.detail;
if (action === "hold" && this.hass.user?.is_admin) {
if (action === "hold" && this._user?.is_admin) {
fireEvent(this, "toggle-edit-mode", true);
return;
}
@@ -376,10 +398,10 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
.deleteLabel=${this._deleteLabel(kind)}
.editMode=${this.editMode ?? false}
.disabled=${this.stateObj.state === UNAVAILABLE}
.isAdmin=${Boolean(this.hass.user?.is_admin)}
.isAdmin=${Boolean(this._user?.is_admin)}
.showDone=${showDone}
.addLabel=${this._localizeFavorite(kind, "add")}
.doneLabel=${this.hass.localize(
.doneLabel=${this._localize(
"ui.dialogs.more_info_control.exit_edit_mode"
)}
@favorite-item-action=${this._handleFavoriteAction}
@@ -415,7 +437,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
${supportsPosition
? this._renderKindSection(
"position",
this.hass.localize("ui.card.cover.position"),
this._localize("ui.card.cover.position"),
this._favoritePositions,
showDoneOnPosition,
showLabels
@@ -424,7 +446,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
${supportsTiltPosition
? this._renderKindSection(
"tilt",
this.hass.localize("ui.card.cover.tilt_position"),
this._localize("ui.card.cover.tilt_position"),
this._favoriteTiltPositions,
true,
showLabels
@@ -1,18 +1,18 @@
import { consume } from "@lit/context";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-absolute-time";
import "../../../components/ha-relative-time";
import type { HomeAssistantFormatters } from "../../../types";
import { formattersContext } from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { LightEntity } from "../../../data/light";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import "../../../panels/lovelace/components/hui-timestamp-display";
import type { HomeAssistant } from "../../../types";
@customElement("ha-more-info-state-header")
export class HaMoreInfoStateHeader extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LightEntity;
@property({ attribute: false }) public stateOverride?: string;
@@ -21,6 +21,10 @@ export class HaMoreInfoStateHeader extends LitElement {
@state() private _absoluteTime = false;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
private _localizeState(): TemplateResult | string {
if (
this.stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
@@ -29,7 +33,6 @@ export class HaMoreInfoStateHeader extends LitElement {
) {
return html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(this.stateObj.state)}
format="relative"
capitalize
@@ -37,7 +40,7 @@ export class HaMoreInfoStateHeader extends LitElement {
`;
}
return this.hass.formatEntityState(this.stateObj);
return this._formatters?.formatEntityState(this.stateObj) ?? "";
}
private _toggleAbsolute() {
@@ -192,7 +192,6 @@ class DialogLightColorFavorite extends DirtyStateProviderMixin<LightColorFavorit
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
@@ -202,7 +201,6 @@ class DialogLightColorFavorite extends DirtyStateProviderMixin<LightColorFavorit
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
@@ -1,14 +1,17 @@
import { consume, type ContextType } from "@lit/context";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { apiContext, configContext } from "../../../../data/context";
import { UNAVAILABLE } from "../../../../data/entity/entity";
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { LightColor, LightEntity } from "../../../../data/light";
import { computeDefaultFavoriteColors } from "../../../../data/light";
import type { HomeAssistant } from "../../../../types";
import { showConfirmationDialog } from "../../../generic/show-dialog-box";
import "../ha-more-info-favorites";
import type { HaMoreInfoFavorites } from "../ha-more-info-favorites";
@@ -23,7 +26,17 @@ declare global {
@customElement("ha-more-info-light-favorite-colors")
export class HaMoreInfoLightFavoriteColors extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@property({ attribute: false }) public stateObj!: LightEntity;
@@ -53,7 +66,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
private _apply(index: number): void {
const favorite = this._favoriteColors[index];
this.hass.callService("light", "turn_on", {
this._api.callService("light", "turn_on", {
entity_id: this.stateObj.entity_id,
...favorite,
});
@@ -61,7 +74,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
private async _save(newFavoriteColors: LightColor[]): Promise<void> {
const result = await updateEntityRegistryEntry(
this.hass,
this._api,
this.entry!.entity_id,
{
options_domain: "light",
@@ -76,7 +89,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
private _add = async (): Promise<void> => {
const color = await showLightColorFavoriteDialog(this, {
entry: this.entry!,
title: this.hass.localize(
title: this._localize(
"ui.dialogs.more_info_control.light.favorite_color.add_title"
),
});
@@ -93,7 +106,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
const color = await showLightColorFavoriteDialog(this, {
entry: this.entry!,
initialColor: this._favoriteColors[index],
title: this.hass.localize(
title: this._localize(
"ui.dialogs.more_info_control.light.favorite_color.edit_title"
),
});
@@ -111,13 +124,13 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
private _delete = async (index: number): Promise<void> => {
const confirm = await showConfirmationDialog(this, {
destructive: true,
title: this.hass.localize(
title: this._localize(
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_title"
),
text: this.hass.localize(
text: this._localize(
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_text"
),
confirmText: this.hass.localize(
confirmText: this._localize(
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_action"
),
});
@@ -136,7 +149,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
editMode: boolean
): TemplateResult =>
html`<ha-favorite-color-button
.label=${this.hass.localize(
.label=${this._localize(
`ui.dialogs.more_info_control.light.favorite_color.${
editMode ? "edit" : "set"
}`,
@@ -147,12 +160,9 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
></ha-favorite-color-button>`;
private _deleteLabel = (index: number): string =>
this.hass.localize(
"ui.dialogs.more_info_control.light.favorite_color.delete",
{
number: index + 1,
}
);
this._localize("ui.dialogs.more_info_control.light.favorite_color.delete", {
number: index + 1,
});
private _handleFavoriteAction = (
ev: HASSDomEvent<HASSDomEvents["favorite-item-action"]>
@@ -161,7 +171,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
const { action, index } = ev.detail;
if (action === "hold" && this.hass.user?.is_admin) {
if (action === "hold" && this._config.user?.is_admin) {
fireEvent(this, "toggle-edit-mode", true);
return;
}
@@ -210,11 +220,11 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
.deleteLabel=${this._deleteLabel as HaMoreInfoFavorites["deleteLabel"]}
.editMode=${this.editMode}
.disabled=${this.stateObj.state === UNAVAILABLE}
.isAdmin=${Boolean(this.hass.user?.is_admin)}
.addLabel=${this.hass.localize(
.isAdmin=${Boolean(this._config.user?.is_admin)}
.addLabel=${this._localize(
"ui.dialogs.more_info_control.light.favorite_color.add"
)}
.doneLabel=${this.hass.localize(
.doneLabel=${this._localize(
"ui.dialogs.more_info_control.exit_edit_mode"
)}
@favorite-item-action=${this._handleFavoriteAction}
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import { mdiEyedropper } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
@@ -10,19 +11,21 @@ import {
rgb2hs,
rgb2hsv,
} from "../../../../common/color/convert-color";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-hs-color-picker";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-labeled-slider";
import { apiContext } from "../../../../data/context";
import type { LightColor, LightEntity } from "../../../../data/light";
import {
getLightCurrentModeRgbColor,
LightColorMode,
lightSupportsColorMode,
} from "../../../../data/light";
import type { HomeAssistant } from "../../../../types";
declare global {
interface HASSDomEvents {
@@ -32,7 +35,13 @@ declare global {
@customElement("light-color-rgb-picker")
class LightRgbColorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public stateObj!: LightEntity;
@@ -109,7 +118,7 @@ class LightRgbColorPicker extends LitElement {
${supportsRgbw || supportsRgbww
? html`<ha-labeled-slider
labeled
.caption=${this.hass.localize("ui.card.light.color_brightness")}
.caption=${this._localize("ui.card.light.color_brightness")}
icon="mdi:brightness-7"
min="0"
max="100"
@@ -121,7 +130,7 @@ class LightRgbColorPicker extends LitElement {
? html`
<ha-labeled-slider
labeled
.caption=${this.hass.localize("ui.card.light.white_value")}
.caption=${this._localize("ui.card.light.white_value")}
icon="mdi:file-word-box"
min="0"
max="100"
@@ -135,7 +144,7 @@ class LightRgbColorPicker extends LitElement {
? html`
<ha-labeled-slider
labeled
.caption=${this.hass.localize("ui.card.light.cold_white_value")}
.caption=${this._localize("ui.card.light.cold_white_value")}
icon="mdi:file-word-box-outline"
min="0"
max="100"
@@ -145,7 +154,7 @@ class LightRgbColorPicker extends LitElement {
></ha-labeled-slider>
<ha-labeled-slider
labeled
.caption=${this.hass.localize("ui.card.light.warm_white_value")}
.caption=${this._localize("ui.card.light.warm_white_value")}
icon="mdi:file-word-box"
min="0"
max="100"
@@ -212,10 +221,7 @@ class LightRgbColorPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (
this._isInteracting ||
(!changedProps.has("entityId") && !changedProps.has("hass"))
) {
if (this._isInteracting || !changedProps.has("stateObj")) {
return;
}
@@ -346,7 +352,7 @@ class LightRgbColorPicker extends LitElement {
private _applyColor(color: LightColor, params?: Record<string, any>) {
fireEvent(this, "color-changed", color);
this.hass.callService("light", "turn_on", {
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -13,11 +14,14 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { stateColorCss } from "../../../../common/entity/state_color";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-control-slider";
import {
apiContext,
internationalizationContext,
} from "../../../../data/context";
import { UNAVAILABLE } from "../../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity/entity_attributes";
import type { LightColor, LightEntity } from "../../../../data/light";
import { LightColorMode } from "../../../../data/light";
import type { HomeAssistant } from "../../../../types";
declare global {
interface HASSDomEvents {
@@ -47,7 +51,13 @@ export const generateColorTemperatureGradient = (min: number, max: number) => {
@customElement("light-color-temp-picker")
class LightColorTempPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@property({ attribute: false }) public stateObj!: LightEntity;
@@ -79,7 +89,7 @@ class LightColorTempPicker extends LitElement {
mode="cursor"
@value-changed=${this._ctColorChanged}
@slider-moved=${this._ctColorCursorMoved}
.label=${this.hass.localize(
.label=${this._i18n.localize(
"ui.dialogs.more_info_control.light.color_temp"
)}
style=${styleMap({
@@ -88,7 +98,7 @@ class LightColorTempPicker extends LitElement {
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.light.color_temp_kelvin}
.locale=${this.hass.locale}
.locale=${this._i18n.locale}
>
</ha-control-slider>
`;
@@ -159,7 +169,7 @@ class LightColorTempPicker extends LitElement {
private _applyColor(color: LightColor, params?: Record<string, any>) {
fireEvent(this, "color-changed", color);
this.hass.callService("light", "turn_on", {
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
@@ -65,9 +65,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
return html`
<ha-dialog
.open=${this._open}
header-title=${this.hass.localize(
"ui.components.siren.advanced_controls"
)}
header-title=${this.hass.localize("ui.components.siren.more_controls")}
@closed=${this._dialogClosed}
>
<div>
@@ -1,11 +1,16 @@
import { consume } from "@lit/context";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { transform } from "../../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-control-button";
import { apiContext, configContext } from "../../../../data/context";
import { UNAVAILABLE } from "../../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity/entity_attributes";
import type {
@@ -13,7 +18,11 @@ import type {
ValveEntityOptions,
} from "../../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConfig,
} from "../../../../types";
import type { ValveEntity } from "../../../../data/valve";
import { DEFAULT_VALVE_FAVORITE_POSITIONS } from "../../../../data/valve";
import { normalizeFavoritePositions } from "../../../../data/favorite_positions";
@@ -37,7 +46,20 @@ type FavoriteLocalizeKey =
@customElement("ha-more-info-valve-favorite-positions")
export class HaMoreInfoValveFavoritePositions extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HomeAssistant["user"]>({
transformer: ({ user }) => user,
})
private _user!: HomeAssistant["user"];
@property({ attribute: false }) public stateObj!: ValveEntity;
@@ -64,7 +86,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
key: FavoriteLocalizeKey,
values?: Record<string, string | number>
): string {
return this.hass.localize(
return this._localize(
`ui.dialogs.more_info_control.valve.favorite_position.${key}`,
values
);
@@ -88,7 +110,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
currentOptions.favorite_positions = this._favoritePositions;
const result = await updateEntityRegistryEntry(
this.hass,
this._api,
this.entry.entity_id,
{
options_domain: "valve",
@@ -122,7 +144,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
return;
}
this.hass.callService("valve", "set_valve_position", {
this._api.callService("valve", "set_valve_position", {
entity_id: this.stateObj.entity_id,
position: favorite,
});
@@ -135,7 +157,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
title: this._localizeFavorite(
value === undefined ? "add_title" : "edit_title"
),
inputLabel: this.hass.localize("ui.card.valve.position"),
inputLabel: this._localize("ui.card.valve.position"),
inputType: "number",
inputMin: "0",
inputMax: "100",
@@ -242,7 +264,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
const { action, index } = ev.detail;
if (action === "hold" && this.hass.user?.is_admin) {
if (action === "hold" && this._user?.is_admin) {
fireEvent(this, "toggle-edit-mode", true);
return;
}
@@ -296,10 +318,10 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
.deleteLabel=${this._deleteLabel}
.editMode=${this.editMode ?? false}
.disabled=${this.stateObj.state === UNAVAILABLE}
.isAdmin=${Boolean(this.hass.user?.is_admin)}
.isAdmin=${Boolean(this._user?.is_admin)}
.showDone=${true}
.addLabel=${this._localizeFavorite("add")}
.doneLabel=${this.hass.localize(
.doneLabel=${this._localize(
"ui.dialogs.more_info_control.exit_edit_mode"
)}
@favorite-item-action=${this._handleFavoriteAction}
@@ -1,34 +1,48 @@
import { consume } from "@lit/context";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { stateColorCss } from "../../../common/entity/state_color";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-state-icon";
import type { AlarmControlPanelEntity } from "../../../data/alarm_control_panel";
import { setProtectedAlarmControlPanelMode } from "../../../data/alarm_control_panel";
import { apiContext } from "../../../data/context";
import "../../../state-control/alarm_control_panel/ha-state-control-alarm_control_panel-modes";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistantApi } from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-alarm_control_panel")
class MoreInfoAlarmControlPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public stateObj?: AlarmControlPanelEntity;
private async _disarm() {
setProtectedAlarmControlPanelMode(
this,
this.hass,
{
callService: this._api.callService,
callWS: this._api.callWS,
localize: this._localize,
},
this.stateObj!,
"disarmed"
);
}
protected render() {
if (!this.hass || !this.stateObj) {
if (!this._localize || !this.stateObj) {
return nothing;
}
@@ -38,7 +52,6 @@ class MoreInfoAlarmControlPanel extends LitElement {
};
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls" style=${styleMap(style)}>
@@ -53,7 +66,6 @@ class MoreInfoAlarmControlPanel extends LitElement {
: html`
<ha-state-control-alarm_control_panel-modes
.stateObj=${this.stateObj}
.hass=${this.hass}
>
</ha-state-control-alarm_control_panel-modes>
`}
@@ -62,7 +74,7 @@ class MoreInfoAlarmControlPanel extends LitElement {
${["triggered", "arming", "pending"].includes(this.stateObj.state)
? html`
<ha-control-button @click=${this._disarm} class="disarm">
${this.hass.localize("ui.card.alarm_control_panel.disarm")}
${this._localize("ui.card.alarm_control_panel.disarm")}
</ha-control-button>
`
: nothing}
@@ -1,27 +1,37 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-button";
import "../../../components/ha-relative-time";
import { apiContext } from "../../../data/context";
import { triggerAutomationActions } from "../../../data/automation";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistantApi } from "../../../types";
@customElement("more-info-automation")
class MoreInfoAutomation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.hass || !this.stateObj) {
if (!this._localize || !this.stateObj) {
return nothing;
}
return html`
<hr />
<div class="flex">
<div>${this.hass.localize("ui.card.automation.last_triggered")}:</div>
<div>${this._localize("ui.card.automation.last_triggered")}:</div>
<ha-relative-time
.datetime=${this.stateObj.attributes.last_triggered}
capitalize
@@ -35,14 +45,14 @@ class MoreInfoAutomation extends LitElement {
@click=${this._runActions}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
${this.hass.localize("ui.card.automation.trigger")}
${this._localize("ui.card.automation.trigger")}
</ha-button>
</div>
`;
}
private _runActions() {
triggerAutomationActions(this.hass, this.stateObj!.entity_id);
triggerAutomationActions(this._api, this.stateObj!.entity_id);
}
static styles = css`
@@ -1,17 +1,27 @@
import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { slugify } from "../../../common/string/slugify";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-camera-stream";
import type { CameraEntity } from "../../../data/camera";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistantApi } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { showToast } from "../../../util/toast";
@customElement("more-info-camera")
class MoreInfoCamera extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@property({ attribute: false }) public stateObj?: CameraEntity;
@@ -30,13 +40,12 @@ class MoreInfoCamera extends LitElement {
}
protected render() {
if (!this._attached || !this.stateObj) {
if (!this._attached || !this.stateObj || !this._localize) {
return nothing;
}
return html`
<ha-camera-stream
.hass=${this.hass}
.stateObj=${this.stateObj}
allow-exoplayer
controls
@@ -49,7 +58,7 @@ class MoreInfoCamera extends LitElement {
.disabled=${this.stateObj.state === UNAVAILABLE}
appearance="filled"
>
${this.hass.localize(
${this._localize(
"ui.dialogs.more_info_control.camera.download_snapshot"
)}
</ha-progress-button>
@@ -62,7 +71,7 @@ class MoreInfoCamera extends LitElement {
this._waiting = true;
try {
const result: Response | undefined = await this.hass.callApiRaw(
const result: Response | undefined = await this._api.callApiRaw(
"GET",
`camera_proxy/${this.stateObj!.entity_id}`
);
@@ -83,7 +92,7 @@ class MoreInfoCamera extends LitElement {
this._waiting = false;
button.actionError();
showToast(this, {
message: this.hass.localize(
message: this._localize(
"ui.dialogs.more_info_control.camera.failed_to_download"
),
});
@@ -1,3 +1,5 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import {
mdiArrowOscillating,
mdiFan,
@@ -8,7 +10,9 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
@@ -22,10 +26,10 @@ import {
climateHvacModeIcon,
compareClimateHvacModes,
} from "../../../data/climate";
import { apiContext, formattersContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import "../../../state-control/climate/ha-state-control-climate-humidity";
import "../../../state-control/climate/ha-state-control-climate-temperature";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -34,15 +38,24 @@ type MainControl = "temperature" | "humidity";
@customElement("more-info-climate")
class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: ClimateEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state() private _mainControl: MainControl = "temperature";
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
@@ -50,7 +63,6 @@ class MoreInfoClimate extends LitElement {
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="fan_mode"
.attributeValue=${value}
@@ -58,7 +70,6 @@ class MoreInfoClimate extends LitElement {
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_mode"
.attributeValue=${value}
@@ -66,7 +77,6 @@ class MoreInfoClimate extends LitElement {
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
@@ -120,13 +130,13 @@ class MoreInfoClimate extends LitElement {
? html`
<div>
<p class="label">
${this.hass.formatEntityAttributeName(
${this._formatters.formatEntityAttributeName(
this.stateObj,
"current_temperature"
)}
</p>
<p class="value">
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
)}
@@ -138,13 +148,13 @@ class MoreInfoClimate extends LitElement {
? html`
<div>
<p class="label">
${this.hass.formatEntityAttributeName(
${this._formatters.formatEntityAttributeName(
this.stateObj,
"current_humidity"
)}
</p>
<p class="value">
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}
@@ -157,7 +167,6 @@ class MoreInfoClimate extends LitElement {
${this._mainControl === "temperature"
? html`
<ha-state-control-climate-temperature
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-climate-temperature>
`
@@ -165,7 +174,6 @@ class MoreInfoClimate extends LitElement {
${this._mainControl === "humidity"
? html`
<ha-state-control-climate-humidity
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-climate-humidity>
`
@@ -176,7 +184,7 @@ class MoreInfoClimate extends LitElement {
<ha-icon-button-toggle
.selected=${this._mainControl === "temperature"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.climate.temperature"
)}
.control=${"temperature"}
@@ -187,7 +195,7 @@ class MoreInfoClimate extends LitElement {
<ha-icon-button-toggle
.selected=${this._mainControl === "humidity"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.climate.humidity"
)}
.control=${"humidity"}
@@ -201,8 +209,7 @@ class MoreInfoClimate extends LitElement {
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.localize("ui.card.climate.mode")}
.label=${this._localize("ui.card.climate.mode")}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
.options=${stateObj.attributes.hvac_modes
@@ -211,7 +218,7 @@ class MoreInfoClimate extends LitElement {
.map((mode) => ({
value: mode,
iconPath: climateHvacModeIcon(mode),
label: this.hass.formatEntityState(stateObj, mode),
label: this._formatters.formatEntityState(stateObj, mode),
}))}
@wa-select=${this._handleOperationModeChanged}
>
@@ -223,8 +230,7 @@ class MoreInfoClimate extends LitElement {
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
@@ -233,7 +239,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handlePresetmodeChanged}
.options=${stateObj.attributes.preset_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
@@ -248,8 +254,7 @@ class MoreInfoClimate extends LitElement {
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
@@ -258,7 +263,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handleFanModeChanged}
.options=${stateObj.attributes.fan_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
@@ -273,8 +278,7 @@ class MoreInfoClimate extends LitElement {
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
@@ -283,7 +287,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handleSwingmodeChanged}
.options=${stateObj.attributes.swing_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
@@ -302,8 +306,7 @@ class MoreInfoClimate extends LitElement {
stateObj.attributes.swing_horizontal_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"swing_horizontal_mode"
)}
@@ -313,7 +316,7 @@ class MoreInfoClimate extends LitElement {
.options=${stateObj.attributes.swing_horizontal_modes.map(
(mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"swing_horizontal_mode",
mode
@@ -403,7 +406,7 @@ class MoreInfoClimate extends LitElement {
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this.hass.callService("climate", service, data);
await this._api.callService("climate", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
@@ -1,3 +1,4 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -5,17 +6,20 @@ import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-markdown";
import "../../../components/input/ha-input";
import type { HomeAssistant } from "../../../types";
import { apiContext } from "../../../data/context";
import type { HomeAssistantApi } from "../../../types";
@customElement("more-info-configurator")
export class MoreInfoConfigurator extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _isConfiguring = false;
private _fieldInput = {};
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
private _fieldInput: Record<string, unknown> = {};
protected render() {
if (this.stateObj?.state !== "configure") {
@@ -71,7 +75,7 @@ export class MoreInfoConfigurator extends LitElement {
this._isConfiguring = true;
this.hass.callService("configurator", "configure", data).then(
this._api.callService("configurator", "configure", data).then(
() => {
this._isConfiguring = false;
},
@@ -1,8 +1,12 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../../../types";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { apiContext } from "../../../data/context";
import type { HomeAssistantApi } from "../../../types";
import "../../../components/ha-assist-chat";
import "../../../components/ha-spinner";
import "../../../components/ha-alert";
@@ -11,14 +15,20 @@ import { getAssistPipeline } from "../../../data/assist_pipeline";
@customElement("more-info-conversation")
class MoreInfoConversation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() public _pipeline?: AssistPipeline;
@state() private _errorLoadAssist?: "not_found" | "unknown";
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
@@ -40,7 +50,7 @@ class MoreInfoConversation extends LitElement {
this._errorLoadAssist = undefined;
const pipelineId = this.stateObj!.entity_id;
try {
const pipeline = await getAssistPipeline(this.hass, pipelineId);
const pipeline = await getAssistPipeline(this._api, pipelineId);
// Verify the pipeline is still the same.
if (this.stateObj && pipelineId === this.stateObj.entity_id) {
this._pipeline = pipeline;
@@ -61,21 +71,20 @@ class MoreInfoConversation extends LitElement {
}
protected render() {
if (!this.hass || !this.stateObj) {
if (!this._localize || !this.stateObj) {
return nothing;
}
return html`
${this._errorLoadAssist
? html`<ha-alert alert-type="error">
${this.hass.localize(
${this._localize(
`ui.dialogs.voice_command.${this._errorLoadAssist}_error_load_assist`
)}
</ha-alert>`
: this._pipeline
? html`
<ha-assist-chat
.hass=${this.hass}
.pipeline=${this._pipeline}
disable-speech
></ha-assist-chat>
@@ -1,18 +1,28 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-button";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistantApi } from "../../../types";
@customElement("more-info-counter")
class MoreInfoCounter extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.hass || !this.stateObj) {
if (!this._localize || !this.stateObj) {
return nothing;
}
@@ -28,7 +38,7 @@ class MoreInfoCounter extends LitElement {
.disabled=${disabled ||
Number(this.stateObj.state) === this.stateObj.attributes.maximum}
>
${this.hass!.localize("ui.card.counter.actions.increment")}
${this._localize("ui.card.counter.actions.increment")}
</ha-button>
<ha-button
appearance="plain"
@@ -38,7 +48,7 @@ class MoreInfoCounter extends LitElement {
.disabled=${disabled ||
Number(this.stateObj.state) === this.stateObj.attributes.minimum}
>
${this.hass!.localize("ui.card.counter.actions.decrement")}
${this._localize("ui.card.counter.actions.decrement")}
</ha-button>
<ha-button
appearance="plain"
@@ -47,7 +57,7 @@ class MoreInfoCounter extends LitElement {
@click=${this._handleActionClick}
.disabled=${disabled}
>
${this.hass!.localize("ui.card.counter.actions.reset")}
${this._localize("ui.card.counter.actions.reset")}
</ha-button>
</div>
`;
@@ -55,7 +65,7 @@ class MoreInfoCounter extends LitElement {
private _handleActionClick(e: MouseEvent): void {
const action = (e.currentTarget as any).action;
this.hass.callService("counter", action, {
this._api.callService("counter", action, {
entity_id: this.stateObj!.entity_id,
});
}
@@ -1,10 +1,14 @@
import { consume } from "@lit/context";
import { mdiMenu, mdiSwapVertical } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
import { formattersContext } from "../../../data/context";
import {
shouldShowFavoriteOptions,
type ExtEntityRegistryEntry,
@@ -21,7 +25,7 @@ import "../../../state-control/cover/ha-state-control-cover-buttons";
import "../../../state-control/cover/ha-state-control-cover-position";
import "../../../state-control/cover/ha-state-control-cover-tilt-position";
import "../../../state-control/cover/ha-state-control-cover-toggle";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistantFormatters } from "../../../types";
import "../components/covers/ha-more-info-cover-favorite-positions";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@@ -30,7 +34,13 @@ type Mode = "position" | "button";
@customElement("more-info-cover")
class MoreInfoCover extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public stateObj?: CoverEntity;
@@ -58,11 +68,11 @@ class MoreInfoCover extends LitElement {
}
private get _stateOverride() {
const stateDisplay = this.hass.formatEntityState(this.stateObj!);
const stateDisplay = this._formatters.formatEntityState(this.stateObj!);
const positionStateDisplay = computeCoverPositionStateDisplay(
this.stateObj!,
this.hass
this._formatters.formatEntityAttributeValue
);
if (positionStateDisplay) {
@@ -72,7 +82,7 @@ class MoreInfoCover extends LitElement {
}
protected render() {
if (!this.hass || !this.stateObj) {
if (!this.stateObj) {
return nothing;
}
@@ -113,7 +123,6 @@ class MoreInfoCover extends LitElement {
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
@@ -126,7 +135,6 @@ class MoreInfoCover extends LitElement {
? html`
<ha-state-control-cover-position
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-cover-position>
`
: nothing}
@@ -134,7 +142,6 @@ class MoreInfoCover extends LitElement {
? html`
<ha-state-control-cover-tilt-position
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-cover-tilt-position>
`
: nothing}
@@ -148,14 +155,12 @@ class MoreInfoCover extends LitElement {
? html`
<ha-state-control-cover-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-cover-toggle>
`
: supportsOpenClose || supportsTilt
? html`
<ha-state-control-cover-buttons
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-cover-buttons>
`
: nothing}
@@ -169,7 +174,7 @@ class MoreInfoCover extends LitElement {
? html`
<ha-icon-button-group>
<ha-icon-button-toggle
.label=${this.hass.localize(
.label=${this._localize(
`ui.dialogs.more_info_control.cover.switch_mode.position`
)}
.selected=${this._mode === "position"}
@@ -178,7 +183,7 @@ class MoreInfoCover extends LitElement {
@click=${this._setMode}
></ha-icon-button-toggle>
<ha-icon-button-toggle
.label=${this.hass.localize(
.label=${this._localize(
`ui.dialogs.more_info_control.cover.switch_mode.button`
)}
.selected=${this._mode === "button"}
@@ -195,7 +200,6 @@ class MoreInfoCover extends LitElement {
showFavoriteControls
? html`
<ha-more-info-cover-favorite-positions
.hass=${this.hass}
.stateObj=${this.stateObj}
.entry=${this.entry}
.editMode=${this.editMode}
@@ -1,18 +1,35 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { setDateValue } from "../../../data/date";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistantApi,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
@customElement("more-info-date")
class MoreInfoDate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
return nothing;
@@ -20,7 +37,7 @@ class MoreInfoDate extends LitElement {
return html`
<ha-date-input
.locale=${this.hass.locale}
.locale=${this._locale}
.value=${this.stateObj.state === UNKNOWN
? undefined
: this.stateObj.state}
@@ -32,7 +49,11 @@ class MoreInfoDate extends LitElement {
private _dateChanged(ev: ValueChangedEvent<string>): void {
if (ev.detail.value) {
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
setDateValue(
this._api.callService,
this.stateObj!.entity_id,
ev.detail.value
);
}
}
@@ -1,19 +1,36 @@
import { consume } from "@lit/context";
import { format } from "date-fns";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { setDateTimeValue } from "../../../data/datetime";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistantApi,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
@customElement("more-info-datetime")
class MoreInfoDatetime extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
return nothing;
@@ -27,14 +44,14 @@ class MoreInfoDatetime extends LitElement {
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
return html`<ha-date-input
.locale=${this.hass.locale}
.locale=${this._locale}
.value=${date}
@value-changed=${this._dateChanged}
>
</ha-date-input>
<ha-time-input
.value=${time}
.locale=${this.hass.locale}
.locale=${this._locale}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>`;
@@ -50,7 +67,11 @@ class MoreInfoDatetime extends LitElement {
const newTime = ev.detail.value.split(":").map(Number);
dateObj.setHours(newTime[0], newTime[1], newTime[2]);
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
setDateTimeValue(
this._api.callService,
this.stateObj!.entity_id,
dateObj
);
}
}
@@ -60,7 +81,11 @@ class MoreInfoDatetime extends LitElement {
const newDate = ev.detail.value.split("-").map(Number);
dateObj.setFullYear(newDate[0], newDate[1] - 1, newDate[2]);
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
setDateTimeValue(
this._api.callService,
this.stateObj!.entity_id,
dateObj
);
}
}
+23 -27
View File
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import {
mdiArrowOscillating,
mdiArrowOscillatingOff,
@@ -26,7 +27,7 @@ import {
import { forwardHaptic } from "../../../data/haptics";
import "../../../state-control/fan/ha-state-control-fan-speed";
import "../../../state-control/ha-state-control-toggle";
import type { HomeAssistant } from "../../../types";
import { apiContext, formattersContext } from "../../../data/context";
import "../components/ha-more-info-control-select-container";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@@ -34,7 +35,13 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("more-info-fan")
class MoreInfoFan extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public stateObj?: FanEntity;
@@ -42,7 +49,6 @@ class MoreInfoFan extends LitElement {
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
@@ -50,7 +56,6 @@ class MoreInfoFan extends LitElement {
private _renderDirectionIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${value}
@@ -59,7 +64,7 @@ class MoreInfoFan extends LitElement {
private _toggle = () => {
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
forwardHaptic(this, "light");
this.hass.callService("fan", service, {
this._api.callService("fan", service, {
entity_id: this.stateObj!.entity_id,
});
};
@@ -70,7 +75,7 @@ class MoreInfoFan extends LitElement {
if (!newVal || oldVal === newVal) return;
this.hass.callService("fan", "set_direction", {
this._api.callService("fan", "set_direction", {
entity_id: this.stateObj!.entity_id,
direction: newVal,
});
@@ -83,7 +88,7 @@ class MoreInfoFan extends LitElement {
if (!newVal || oldVal === newVal) return;
this._presetMode = newVal;
this.hass.callService("fan", "set_preset_mode", {
this._api.callService("fan", "set_preset_mode", {
entity_id: this.stateObj!.entity_id,
preset_mode: newVal,
});
@@ -95,7 +100,7 @@ class MoreInfoFan extends LitElement {
if (oldVal === newVal) return;
this.hass.callService("fan", "oscillate", {
this._api.callService("fan", "oscillate", {
entity_id: this.stateObj!.entity_id,
oscillating: newVal,
});
@@ -108,11 +113,11 @@ class MoreInfoFan extends LitElement {
}
private get _stateOverride() {
const stateDisplay = this.hass.formatEntityState(this.stateObj!);
const stateDisplay = this._formatters.formatEntityState(this.stateObj!);
const positionStateDisplay = computeFanSpeedStateDisplay(
this.stateObj!,
this.hass
this._formatters
);
if (positionStateDisplay && stateActive(this.stateObj!)) {
@@ -122,7 +127,7 @@ class MoreInfoFan extends LitElement {
}
protected render() {
if (!this.hass || !this.stateObj) {
if (!this.stateObj) {
return nothing;
}
@@ -150,23 +155,18 @@ class MoreInfoFan extends LitElement {
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
<div class="controls">
${supportsSpeed
? html`
<ha-state-control-fan-speed
.stateObj=${this.stateObj}
.hass=${this.hass}
>
<ha-state-control-fan-speed .stateObj=${this.stateObj}>
</ha-state-control-fan-speed>
`
: html`
<ha-state-control-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
.iconPathOn=${mdiFan}
.iconPathOff=${mdiFanOff}
></ha-state-control-toggle>
@@ -192,8 +192,7 @@ class MoreInfoFan extends LitElement {
${supportsPresetMode && this.stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this.stateObj,
"preset_mode"
)}
@@ -203,7 +202,7 @@ class MoreInfoFan extends LitElement {
.options=${this.stateObj.attributes.preset_modes.map(
(mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
@@ -219,8 +218,7 @@ class MoreInfoFan extends LitElement {
${supportsDirection
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this.stateObj,
"direction"
)}
@@ -230,7 +228,7 @@ class MoreInfoFan extends LitElement {
.options=${["forward", "reverse"].map((direction) => ({
value: direction,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
? this._formatters.formatEntityAttributeValue(
this.stateObj,
"direction",
direction
@@ -241,7 +239,6 @@ class MoreInfoFan extends LitElement {
>
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${this.stateObj.attributes.direction}
@@ -252,8 +249,7 @@ class MoreInfoFan extends LitElement {
${supportsOscillate
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this.stateObj,
"oscillating"
)}
@@ -269,7 +265,7 @@ class MoreInfoFan extends LitElement {
? mdiArrowOscillating
: mdiArrowOscillatingOff,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
? this._formatters.formatEntityAttributeValue(
this.stateObj,
"oscillating",
val === "true"
@@ -1,31 +1,44 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiPower, mdiTuneVariant } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-list-item";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { apiContext, formattersContext } from "../../../data/context";
import type { HumidifierEntity } from "../../../data/humidifier";
import { HumidifierEntityFeature } from "../../../data/humidifier";
import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("more-info-humidifier")
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HumidifierEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state() public _mode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="mode"
.attributeValue=${value}
@@ -43,7 +56,6 @@ class MoreInfoHumidifier extends LitElement {
return nothing;
}
const hass = this.hass;
const stateObj = this.stateObj;
const supportModes = supportsFeature(
@@ -57,13 +69,13 @@ class MoreInfoHumidifier extends LitElement {
? html`
<div>
<p class="label">
${this.hass.formatEntityAttributeName(
${this._formatters.formatEntityAttributeName(
this.stateObj,
"current_humidity"
)}
</p>
<p class="value">
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}
@@ -75,22 +87,20 @@ class MoreInfoHumidifier extends LitElement {
<div class="controls">
<ha-state-control-humidifier-humidity
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-humidifier-humidity>
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.hass=${hass}
.label=${this.hass.localize("ui.card.humidifier.state")}
.label=${this._localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleStateChanged}
.options=${["off", "on"].map((fanState) => ({
value: fanState,
label: this.stateObj
? this.hass.formatEntityState(this.stateObj, fanState)
? this._formatters.formatEntityState(this.stateObj, fanState)
: fanState,
}))}
>
@@ -100,15 +110,14 @@ class MoreInfoHumidifier extends LitElement {
${supportModes
? html`
<ha-control-select-menu
.hass=${hass}
.label=${hass.localize("ui.card.humidifier.mode")}
.label=${this._localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleModeChanged}
.options=${stateObj.attributes.available_modes?.map((mode) => ({
value: mode,
label: stateObj
? this.hass.formatEntityAttributeValue(
? this._formatters.formatEntityAttributeValue(
stateObj,
"mode",
mode
@@ -164,7 +173,7 @@ class MoreInfoHumidifier extends LitElement {
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this.hass.callService("humidifier", service, data);
await this._api.callService("humidifier", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
@@ -1,18 +1,21 @@
import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-camera-stream";
import { customElement, property, state } from "lit/decorators";
import { connectionContext } from "../../../data/context";
import type { ImageEntity } from "../../../data/image";
import { computeImageUrl } from "../../../data/image";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistantConnection } from "../../../types";
@customElement("more-info-image")
class MoreInfoImage extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: HomeAssistantConnection;
@property({ attribute: false }) public stateObj?: ImageEntity;
protected render() {
if (!this.hass || !this.stateObj) {
if (!this._connection || !this.stateObj) {
return nothing;
}
const imageUrl = computeImageUrl(this.stateObj);
@@ -21,7 +24,7 @@ class MoreInfoImage extends LitElement {
}
return html`<img
alt=${this.stateObj.attributes.friendly_name || this.stateObj.entity_id}
src=${this.hass.hassUrl(imageUrl)}
src=${this._connection.hassUrl(imageUrl)}
/> `;
}
@@ -2,32 +2,31 @@ import { mdiPower, mdiPowerOff } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../state-control/ha-state-control-toggle";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-input_boolean")
class MoreInfoInputBoolean extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@property({ attribute: false }) public stateObj?: HassEntity;
protected render() {
if (!this.hass || !this.stateObj) {
if (!this._localize || !this.stateObj) {
return nothing;
}
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls">
<ha-state-control-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
.iconPathOn=${mdiPower}
.iconPathOff=${mdiPowerOff}
></ha-state-control-toggle>
@@ -1,21 +1,38 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import {
setInputDateTimeValue,
stateToIsoDateString,
} from "../../../data/input_datetime";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistantApi,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
@customElement("more-info-input_datetime")
class MoreInfoInputDatetime extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -25,7 +42,7 @@ class MoreInfoInputDatetime extends LitElement {
${this.stateObj.attributes.has_date
? html`
<ha-date-input
.locale=${this.hass.locale}
.locale=${this._locale}
.value=${stateToIsoDateString(this.stateObj)}
.disabled=${this.stateObj.state === UNAVAILABLE}
@value-changed=${this._dateChanged}
@@ -41,7 +58,7 @@ class MoreInfoInputDatetime extends LitElement {
: this.stateObj.attributes.has_date
? this.stateObj.state.split(" ")[1]
: this.stateObj.state}
.locale=${this.hass.locale}
.locale=${this._locale}
.disabled=${this.stateObj.state === UNAVAILABLE}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
@@ -57,7 +74,7 @@ class MoreInfoInputDatetime extends LitElement {
private _timeChanged(ev: ValueChangedEvent<string>): void {
setInputDateTimeValue(
this.hass!,
this._api.callService,
this.stateObj!.entity_id,
ev.detail.value,
this.stateObj!.attributes.has_date
@@ -68,7 +85,7 @@ class MoreInfoInputDatetime extends LitElement {
private _dateChanged(ev: ValueChangedEvent<string>): void {
setInputDateTimeValue(
this.hass!,
this._api.callService,
this.stateObj!.entity_id,
this.stateObj!.attributes.has_time
? this.stateObj!.state.split(" ")[1]
@@ -1,7 +1,8 @@
import { consume } from "@lit/context";
import { mdiHomeImportOutline, mdiPause, mdiPlay } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -10,6 +11,12 @@ import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import {
apiContext,
entitiesContext,
internationalizationContext,
statesContext,
} from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../../data/entity/entity_registry";
import {
@@ -25,13 +32,31 @@ import {
isMowing,
} from "../../../data/lawn_mower";
import "../../../state-control/lawn_mower/ha-state-control-lawn_mower-status";
import type { HomeAssistant } from "../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-lawn_mower")
class MoreInfoLawnMower extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HomeAssistant["states"];
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities!: HomeAssistant["entities"];
@property({ attribute: false }) public stateObj?: LawnMowerEntity;
@@ -62,11 +87,11 @@ class MoreInfoLawnMower extends LitElement {
}
private get _startPauseLabel(): string {
if (!this.stateObj || !this.hass) return "";
if (!this.stateObj) return "";
return isMowing(this.stateObj) &&
supportsFeature(this.stateObj, LawnMowerEntityFeature.PAUSE)
? this.hass.localize("ui.dialogs.more_info_control.lawn_mower.pause")
: this.hass.localize(
? this._i18n.localize("ui.dialogs.more_info_control.lawn_mower.pause")
: this._i18n.localize(
"ui.dialogs.more_info_control.lawn_mower.start_mowing"
);
}
@@ -79,18 +104,18 @@ class MoreInfoLawnMower extends LitElement {
}
private _renderBattery() {
if (!this.stateObj || !this.hass) {
if (!this.stateObj) {
return nothing;
}
const deviceId = this.hass.entities[this.stateObj.entity_id]?.device_id;
const deviceId = this._entities[this.stateObj.entity_id]?.device_id;
const entities = deviceId
? this._deviceEntities(deviceId, this.hass.entities)
? this._deviceEntities(deviceId, this._entities)
: [];
const batteryEntity = findBatteryEntity(this.hass, entities);
const batteryEntity = findBatteryEntity(this._states, entities);
const battery = batteryEntity
? this.hass.states[batteryEntity.entity_id]
? this._states[batteryEntity.entity_id]
: undefined;
const batteryDomain = battery ? computeStateDomain(battery) : undefined;
@@ -99,11 +124,11 @@ class MoreInfoLawnMower extends LitElement {
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
) {
const batteryChargingEntity = findBatteryChargingEntity(
this.hass,
this._states,
entities
);
const batteryCharging = batteryChargingEntity
? this.hass.states[batteryChargingEntity?.entity_id]
? this._states[batteryChargingEntity?.entity_id]
: undefined;
return html`
@@ -112,11 +137,10 @@ class MoreInfoLawnMower extends LitElement {
? nothing
: html`<span
>${Number(battery.state).toFixed()}${blankBeforePercent(
this.hass.locale
this._i18n.locale
)}%</span
>`}
<ha-battery-icon
.hass=${this.hass}
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryCharging}
></ha-battery-icon>
@@ -131,11 +155,11 @@ class MoreInfoLawnMower extends LitElement {
if (!this.stateObj) return;
forwardHaptic(this, "light");
if (isMowing(this.stateObj)) {
this.hass.callService("lawn_mower", "pause", {
this._api.callService("lawn_mower", "pause", {
entity_id: this.stateObj.entity_id,
});
} else {
this.hass.callService("lawn_mower", "start_mowing", {
this._api.callService("lawn_mower", "start_mowing", {
entity_id: this.stateObj.entity_id,
});
}
@@ -144,13 +168,13 @@ class MoreInfoLawnMower extends LitElement {
private _handleDock() {
if (!this.stateObj) return;
forwardHaptic(this, "light");
this.hass.callService("lawn_mower", "dock", {
this._api.callService("lawn_mower", "dock", {
entity_id: this.stateObj.entity_id,
});
}
protected render() {
if (!this.stateObj) {
if (!this.stateObj || !this._i18n) {
return nothing;
}
@@ -161,14 +185,13 @@ class MoreInfoLawnMower extends LitElement {
const hasAnyCommand = this._supportsStartPause || supportsDock;
return html`
<ha-more-info-state-header .hass=${this.hass} .stateObj=${this.stateObj}>
<ha-more-info-state-header .stateObj=${this.stateObj}>
${this._renderBattery()}
</ha-more-info-state-header>
<div class="controls">
<ha-state-control-lawn_mower-status
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-lawn_mower-status>
${hasAnyCommand
@@ -191,7 +214,7 @@ class MoreInfoLawnMower extends LitElement {
${supportsDock
? html`
<ha-control-button
.label=${this.hass.localize(
.label=${this._i18n.localize(
"ui.dialogs.more_info_control.lawn_mower.dock"
)}
@click=${this._handleDock}
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import {
mdiBrightness6,
mdiCreation,
@@ -9,7 +10,9 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
@@ -32,7 +35,7 @@ import {
} from "../../../data/light";
import "../../../state-control/ha-state-control-toggle";
import "../../../state-control/light/ha-state-control-light-brightness";
import type { HomeAssistant } from "../../../types";
import { apiContext, formattersContext } from "../../../data/context";
import "../components/ha-more-info-control-select-container";
import "../components/ha-more-info-state-header";
import "../components/lights/ha-favorite-color-button";
@@ -46,7 +49,17 @@ type MainControl = "brightness" | "color_temp" | "color";
@customElement("more-info-light")
class MoreInfoLight extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public stateObj?: LightEntity;
@@ -60,7 +73,6 @@ class MoreInfoLight extends LitElement {
private _renderEffectIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${value}
@@ -84,13 +96,16 @@ class MoreInfoLight extends LitElement {
private get _stateOverride() {
if (this.stateObj?.attributes.brightness) {
return this.hass.formatEntityAttributeValue(this.stateObj!, "brightness");
return this._formatters.formatEntityAttributeValue(
this.stateObj!,
"brightness"
);
}
return undefined;
}
protected render() {
if (!this.hass || !this.stateObj) {
if (!this.stateObj) {
return nothing;
}
@@ -124,7 +139,6 @@ class MoreInfoLight extends LitElement {
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
@@ -133,7 +147,6 @@ class MoreInfoLight extends LitElement {
? html`
<ha-state-control-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
.iconPathOn=${mdiLightbulbOn}
.iconPathOff=${mdiLightbulbOff}
></ha-state-control-toggle>
@@ -145,26 +158,19 @@ class MoreInfoLight extends LitElement {
? html`
<ha-state-control-light-brightness
.stateObj=${this.stateObj}
.hass=${this.hass}
>
</ha-state-control-light-brightness>
`
: nothing}
${supportsColor && this._mainControl === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
>
<light-color-rgb-picker .stateObj=${this.stateObj}>
</light-color-rgb-picker>
`
: nothing}
${supportsColorTemp && this._mainControl === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
>
<light-color-temp-picker .stateObj=${this.stateObj}>
</light-color-temp-picker>
`
: nothing}
@@ -173,7 +179,7 @@ class MoreInfoLight extends LitElement {
? html`
<ha-icon-button
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.light.toggle"
)}
@click=${this._toggle}
@@ -188,7 +194,7 @@ class MoreInfoLight extends LitElement {
<ha-icon-button-toggle
.selected=${this._mainControl === "brightness"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this.stateObj,
"brightness"
)}
@@ -205,7 +211,7 @@ class MoreInfoLight extends LitElement {
border-only
.selected=${this._mainControl === "color"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.light.color"
)}
.control=${"color"}
@@ -221,7 +227,7 @@ class MoreInfoLight extends LitElement {
border-only
.selected=${this._mainControl === "color_temp"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.light.color_temp"
)}
.control=${"color_temp"}
@@ -236,7 +242,7 @@ class MoreInfoLight extends LitElement {
<div class="separator"></div>
<ha-icon-button
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.light.set_white"
)}
@click=${this._setWhite}
@@ -249,7 +255,6 @@ class MoreInfoLight extends LitElement {
${showFavoriteColors
? html`
<ha-more-info-light-favorite-colors
.hass=${this.hass}
.stateObj=${this.stateObj}
.entry=${this.entry}
.editMode=${this.editMode}
@@ -266,8 +271,7 @@ class MoreInfoLight extends LitElement {
${supportsEffects && this.stateObj.attributes.effect_list
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this.stateObj,
"effect"
)}
@@ -278,7 +282,7 @@ class MoreInfoLight extends LitElement {
(effect) => ({
value: effect,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
? this._formatters.formatEntityAttributeValue(
this.stateObj,
"effect",
effect
@@ -300,13 +304,13 @@ class MoreInfoLight extends LitElement {
private _toggle = () => {
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
forwardHaptic(this, "light");
this.hass.callService("light", service, {
this._api.callService("light", service, {
entity_id: this.stateObj!.entity_id,
});
};
private _setWhite = () => {
this.hass.callService("light", "turn_on", {
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
white: true,
});
@@ -318,7 +322,7 @@ class MoreInfoLight extends LitElement {
if (!newVal || oldVal === newVal) return;
this.hass.callService("light", "turn_on", {
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
effect: newVal,
});
@@ -1,14 +1,18 @@
import { consume } from "@lit/context";
import { mdiCheck } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-outlined-icon-button";
import "../../../components/ha-state-icon";
import { apiContext } from "../../../data/context";
import type { LockEntity } from "../../../data/lock";
import {
LockEntityFeature,
@@ -17,7 +21,7 @@ import {
isJammed,
} from "../../../data/lock";
import "../../../state-control/lock/ha-state-control-lock-toggle";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistantApi } from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@@ -28,7 +32,13 @@ type ButtonState = "normal" | "confirm" | "done";
@customElement("more-info-lock")
class MoreInfoLock extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public stateObj?: LockEntity;
@@ -52,7 +62,16 @@ class MoreInfoLock extends LitElement {
return;
}
callProtectedLockService(this, this.hass, this.stateObj!, "open");
callProtectedLockService(
this,
{
callService: this._api.callService,
callWS: this._api.callWS,
localize: this._localize,
},
this.stateObj!,
"open"
);
this._setButtonState("done", DONE_TIMEOUT_SECOND);
}
@@ -67,15 +86,33 @@ class MoreInfoLock extends LitElement {
}
private async _lock() {
callProtectedLockService(this, this.hass, this.stateObj!, "lock");
callProtectedLockService(
this,
{
callService: this._api.callService,
callWS: this._api.callWS,
localize: this._localize,
},
this.stateObj!,
"lock"
);
}
private async _unlock() {
callProtectedLockService(this, this.hass, this.stateObj!, "unlock");
callProtectedLockService(
this,
{
callService: this._api.callService,
callWS: this._api.callWS,
localize: this._localize,
},
this.stateObj!,
"unlock"
);
}
protected render() {
if (!this.hass || !this.stateObj) {
if (!this._localize || !this.stateObj) {
return nothing;
}
@@ -88,7 +125,6 @@ class MoreInfoLock extends LitElement {
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls" style=${styleMap(style)}>
@@ -105,7 +141,6 @@ class MoreInfoLock extends LitElement {
<ha-state-control-lock-toggle
@lock-service-called=${this._resetButtonState}
.stateObj=${this.stateObj}
.hass=${this.hass}
>
</ha-state-control-lock-toggle>
`}
@@ -116,7 +151,7 @@ class MoreInfoLock extends LitElement {
? html`
<p class="open-done">
<ha-svg-icon path=${mdiCheck}></ha-svg-icon>
${this.hass.localize("ui.card.lock.open_door_done")}
${this._localize("ui.card.lock.open_door_done")}
</p>
`
: html`
@@ -126,8 +161,8 @@ class MoreInfoLock extends LitElement {
@click=${this._open}
>
${this._buttonState === "confirm"
? this.hass.localize("ui.card.lock.open_door_confirm")
: this.hass.localize("ui.card.lock.open_door")}
? this._localize("ui.card.lock.open_door_confirm")
: this._localize("ui.card.lock.open_door")}
</ha-control-button>
`}
</div>
@@ -139,10 +174,10 @@ class MoreInfoLock extends LitElement {
? html`
<ha-control-button-group class="jammed">
<ha-control-button @click=${this._unlock}>
${this.hass.localize("ui.card.lock.unlock")}
${this._localize("ui.card.lock.unlock")}
</ha-control-button>
<ha-control-button @click=${this._lock}>
${this.hass.localize("ui.card.lock.lock")}
${this._localize("ui.card.lock.lock")}
</ha-control-button>
</ha-control-button-group>
`

Some files were not shown because too many files have changed in this diff Show More